From 52918b04f661fae7ba7ce9a63b80b8a4c43edddc Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 2 Jul 2021 23:09:04 +0000 Subject: [PATCH 01/31] draft --- checker/check_request.go | 1 + checker/check_result.go | 24 +++++++++++++++++++++++ checker/check_runner.go | 13 ++++++++++++- checks/binary_artifact.go | 23 ++++++++++++++-------- checks/checkforcontent.go | 41 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 92 insertions(+), 10 deletions(-) diff --git a/checker/check_request.go b/checker/check_request.go index 004d0fd7ef9..7d40f334f27 100644 --- a/checker/check_request.go +++ b/checker/check_request.go @@ -31,5 +31,6 @@ type CheckRequest struct { HTTPClient *http.Client RepoClient clients.RepoClient Logf func(s string, f ...interface{}) + Logf2 func(typ int, code, desc string, f ...interface{}) Owner, Repo string } diff --git a/checker/check_result.go b/checker/check_result.go index ccce9c9ae02..95824e5b53e 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -16,6 +16,7 @@ package checker import ( "errors" + "fmt" scorecarderrors "github.com/ossf/scorecard/errors" ) @@ -25,10 +26,33 @@ const MaxResultConfidence = 10 // ErrorDemoninatorZero indicates the denominator for a proportional result is 0. var ErrorDemoninatorZero = errors.New("internal error: denominator is 0") +// Types of details. +const ( + DetailFail = 0 + DetailPass = 1 + DetailInfo = 2 +) + +// CheckDetail contains information for each detail. +//nolint:govet +type CheckDetail struct { + Type int // Any of DetailFail, DetailPass, DetailInfo. + Code string // A 4 digit string identifying the code, e.g. for remediation. + Desc string // A short string representation of the information. +} + +func (cd *CheckDetail) Validate() { + if cd.Type < DetailFail || + cd.Type > DetailInfo { + panic(fmt.Sprintf("invalid CheckDetail type: %v", cd.Type)) + } +} + type CheckResult struct { Error error `json:"-"` Name string Details []string + Details2 []CheckDetail Confidence int Pass bool ShouldRetry bool `json:"-"` diff --git a/checker/check_runner.go b/checker/check_runner.go index a9843dd728a..9831d96523d 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -40,7 +40,16 @@ type CheckFn func(*CheckRequest) CheckResult type CheckNameToFnMap map[string]CheckFn type logger struct { - messages []string + messages []string + messages2 []CheckDetail +} + +type Logf2 func(typ int, code, desc string, args ...interface{}) + +func (l *logger) Logf2(typ int, code, desc string, args ...interface{}) { + cd := CheckDetail{Type: typ, Code: code, Desc: fmt.Sprintf(desc, args...)} + cd.Validate() + l.messages2 = append(l.messages2, cd) } func (l *logger) Logf(s string, f ...interface{}) { @@ -75,6 +84,7 @@ func (r *Runner) Run(ctx context.Context, f CheckFn) CheckResult { checkRequest.Ctx = ctx l = logger{} checkRequest.Logf = l.Logf + checkRequest.Logf2 = l.Logf2 res = f(&checkRequest) if res.ShouldRetry && !strings.Contains(res.Error.Error(), "invalid header field value") { checkRequest.Logf("error, retrying: %s", res.Error) @@ -83,6 +93,7 @@ func (r *Runner) Run(ctx context.Context, f CheckFn) CheckResult { break } res.Details = l.messages + res.Details2 = l.messages2 if err := logStats(ctx, startTime, res); err != nil { panic(err) diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index e607928e148..a5b8d481d78 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -26,18 +26,25 @@ import ( //nolint func init() { - registerCheck(CheckBinaryArtifacts, BinaryArtifacts) + registerCheck(checkBinaryArtifacts, binaryArtifacts) } -const CheckBinaryArtifacts string = "Binary-Artifacts" +const checkBinaryArtifacts string = "Binary-Artifacts" // BinaryArtifacts will check the repository if it contains binary artifacts. -func BinaryArtifacts(c *checker.CheckRequest) checker.CheckResult { - return CheckFilesContent(CheckBinaryArtifacts, "*", false, c, checkBinaryFileContent) +func binaryArtifacts(c *checker.CheckRequest) checker.CheckResult { + r, err := CheckFilesContent2("*", false, c, checkBinaryFileContent) + if err != nil || !r { + // TODO: we're losing the RetryError, should be handled by caller. + return checker.MakeFailResult(checkBinaryArtifacts, err) + } + + // We're confident it's correct. + return checker.MakePassResult(checkBinaryArtifacts) } func checkBinaryFileContent(path string, content []byte, - logf func(s string, f ...interface{})) (bool, error) { + l checker.Logf2) (bool, error) { binaryFileTypes := map[string]bool{ "crx": true, "deb": true, @@ -82,11 +89,11 @@ func checkBinaryFileContent(path string, content []byte, } if _, ok := binaryFileTypes[t.Extension]; ok { - logf("!! binary-artifact %s", path) + l(checker.DetailFail, "1234", "binary-artifact found: %s", path) return false, nil } else if _, ok := binaryFileTypes[filepath.Ext(path)]; ok { - // falling back to file based extension. - logf("!! binary-artifact %s", path) + // Falling back to file based extension. + l(checker.DetailFail, "1234", "binary-artifact found: %s", path) return false, nil } diff --git a/checks/checkforcontent.go b/checks/checkforcontent.go index 66d7242d334..14f57cee0b5 100644 --- a/checks/checkforcontent.go +++ b/checks/checkforcontent.go @@ -99,10 +99,49 @@ func CheckFilesContent(checkName, shellPathFnPattern string, res = false } } - if res { return checker.MakePassResult(checkName) } return checker.MakeFailResult(checkName, nil) } + +func CheckFilesContent2(shellPathFnPattern string, + caseSensitive bool, + c *checker.CheckRequest, + onFileContent func(path string, content []byte, + l checker.Logf2) (bool, error), +) (bool, error) { + predicate := func(filepath string) bool { + // Filter out Scorecard's own test files. + if isScorecardTestFile(c.Owner, c.Repo, filepath) { + return false + } + // Filter out files based on path/names using the pattern. + b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive) + if err != nil { + c.Logf("error during isMatchingPath: %v", err) + return false + } + return b + } + res := true + for _, file := range c.RepoClient.ListFiles(predicate) { + content, err := c.RepoClient.GetFileContent(file) + if err != nil { + return false, err + } + + rr, err := onFileContent(file, content, c.Logf2) + if err != nil { + return false, err + } + // We don't return rightway to let the onFileContent() + // handler log. + if !rr { + res = false + } + } + + return res, nil +} From d790d19bece655072225a54376370c65b1aa8958 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 2 Jul 2021 23:35:38 +0000 Subject: [PATCH 02/31] save --- checker/check_result.go | 9 +++++---- checks/binary_artifact.go | 17 +++++++++++------ checks/checkforcontent.go | 2 +- checks/checks2.yaml | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 checks/checks2.yaml diff --git a/checker/check_result.go b/checker/check_result.go index 95824e5b53e..a9f5c157487 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -28,9 +28,10 @@ var ErrorDemoninatorZero = errors.New("internal error: denominator is 0") // Types of details. const ( - DetailFail = 0 - DetailPass = 1 - DetailInfo = 2 + DetailFail = 0 + DetailPass = 1 + DetailInfo = 2 + DetailWarning = 3 ) // CheckDetail contains information for each detail. @@ -43,7 +44,7 @@ type CheckDetail struct { func (cd *CheckDetail) Validate() { if cd.Type < DetailFail || - cd.Type > DetailInfo { + cd.Type > DetailWarning { panic(fmt.Sprintf("invalid CheckDetail type: %v", cd.Type)) } } diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index a5b8d481d78..8df0477b6ca 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -26,21 +26,26 @@ import ( //nolint func init() { - registerCheck(checkBinaryArtifacts, binaryArtifacts) + registerCheck(checkBinaryArtifactsName, binaryArtifacts) } -const checkBinaryArtifacts string = "Binary-Artifacts" +// TODO: read from file? +const ( + checkBinaryArtifactsNumber string = "0001" + checkBinaryArtifactsName string = "Binary-Artifacts" +) // BinaryArtifacts will check the repository if it contains binary artifacts. func binaryArtifacts(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*", false, c, checkBinaryFileContent) if err != nil || !r { // TODO: we're losing the RetryError, should be handled by caller. - return checker.MakeFailResult(checkBinaryArtifacts, err) + return checker.MakeFailResult(checkBinaryArtifactsName, err) } // We're confident it's correct. - return checker.MakePassResult(checkBinaryArtifacts) + c.Logf2(checker.DetailPass, checkBinaryArtifactsNumber, "no binary files found in the repo") + return checker.MakePassResult(checkBinaryArtifactsName) } func checkBinaryFileContent(path string, content []byte, @@ -89,11 +94,11 @@ func checkBinaryFileContent(path string, content []byte, } if _, ok := binaryFileTypes[t.Extension]; ok { - l(checker.DetailFail, "1234", "binary-artifact found: %s", path) + l(checker.DetailFail, checkBinaryArtifactsNumber+"-E01", "binary-artifact found: %s", path) return false, nil } else if _, ok := binaryFileTypes[filepath.Ext(path)]; ok { // Falling back to file based extension. - l(checker.DetailFail, "1234", "binary-artifact found: %s", path) + l(checker.DetailFail, checkBinaryArtifactsNumber+"-E01", "binary-artifact found: %s", path) return false, nil } diff --git a/checks/checkforcontent.go b/checks/checkforcontent.go index 14f57cee0b5..a0cf4ee9c44 100644 --- a/checks/checkforcontent.go +++ b/checks/checkforcontent.go @@ -120,7 +120,7 @@ func CheckFilesContent2(shellPathFnPattern string, // Filter out files based on path/names using the pattern. b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive) if err != nil { - c.Logf("error during isMatchingPath: %v", err) + c.Logf2(checker.DetailWarning, "4567", "internal error: %v", err) return false } return b diff --git a/checks/checks2.yaml b/checks/checks2.yaml new file mode 100644 index 00000000000..1ad70dd730f --- /dev/null +++ b/checks/checks2.yaml @@ -0,0 +1,36 @@ +# 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. + +# This is the source of truth for all check descriptions and remediation steps. +# Run `cd checks/main && go run /main` to generate `checks.json` and `checks.md`. +checks: + Binary-Artifacts: + code: C0001 + description: >- + This check tries to determine if a project has binary artifacts in the source repository. + These binaries could be compromised artifacts. Building from the source is recommended. + remediation: + - >- + Remove the binary artifacts from the repository. + errors: + E01: + description: >- + A binary file is found in the repository. + remediation: + - >- + Remove the binary from the repository code. + - >- + Build from the source. + + From a61fc3ec1918b7c5c2c06940c7c7063d732a55ff Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Sat, 3 Jul 2021 01:22:13 +0000 Subject: [PATCH 03/31] fixes --- checker/check_request.go | 2 +- checker/check_result.go | 28 +++++++++++++--- checker/check_runner.go | 32 +++++++++++++++--- checks/automatic_dependency_update.go | 24 +++++++++----- checks/binary_artifact.go | 21 +++++------- checks/checkforcontent.go | 6 ++-- checks/checkforfile.go | 16 +++++++++ checks/checks2.yaml | 23 ++++++++++++- checks/code_review.go | 47 +++++++++++++++------------ cmd/root.go | 1 + pkg/scorecard_result.go | 13 ++++++++ 11 files changed, 156 insertions(+), 57 deletions(-) diff --git a/checker/check_request.go b/checker/check_request.go index 7d40f334f27..bb54e22819b 100644 --- a/checker/check_request.go +++ b/checker/check_request.go @@ -31,6 +31,6 @@ type CheckRequest struct { HTTPClient *http.Client RepoClient clients.RepoClient Logf func(s string, f ...interface{}) - Logf2 func(typ int, code, desc string, f ...interface{}) + CLogger CheckLogger Owner, Repo string } diff --git a/checker/check_result.go b/checker/check_result.go index a9f5c157487..33480c5df1c 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -28,10 +28,10 @@ var ErrorDemoninatorZero = errors.New("internal error: denominator is 0") // Types of details. const ( - DetailFail = 0 - DetailPass = 1 - DetailInfo = 2 - DetailWarning = 3 + DetailFail = 0 + DetailPass = 1 + DetailInfo = 2 + DetailWarn = 3 ) // CheckDetail contains information for each detail. @@ -44,11 +44,18 @@ type CheckDetail struct { func (cd *CheckDetail) Validate() { if cd.Type < DetailFail || - cd.Type > DetailWarning { + cd.Type > DetailWarn { panic(fmt.Sprintf("invalid CheckDetail type: %v", cd.Type)) } } +// Types of results. +const ( + ResultPass = 0 + ResultFail = 1 + ResultDontKnow = 2 +) + type CheckResult struct { Error error `json:"-"` Name string @@ -56,6 +63,7 @@ type CheckResult struct { Details2 []CheckDetail Confidence int Pass bool + Pass2 int ShouldRetry bool `json:"-"` } @@ -64,6 +72,7 @@ func MakeInconclusiveResult(name string, err error) CheckResult { Name: name, Pass: false, Confidence: 0, + Pass2: ResultDontKnow, Error: scorecarderrors.MakeZeroConfidenceError(err), } } @@ -72,6 +81,7 @@ func MakePassResult(name string) CheckResult { return CheckResult{ Name: name, Pass: true, + Pass2: ResultPass, Confidence: MaxResultConfidence, } } @@ -80,6 +90,7 @@ func MakeFailResult(name string, err error) CheckResult { return CheckResult{ Name: name, Pass: false, + Pass2: ResultFail, Confidence: MaxResultConfidence, Error: err, } @@ -89,11 +100,14 @@ func MakeRetryResult(name string, err error) CheckResult { return CheckResult{ Name: name, Pass: false, + Pass2: ResultDontKnow, ShouldRetry: true, Error: scorecarderrors.MakeRetryError(err), } } +// TODO: update this function to return a ResultDontKnow +// if the confidence is low? func MakeProportionalResult(name string, numerator int, denominator int, threshold float32) CheckResult { if denominator == 0 { @@ -103,6 +117,7 @@ func MakeProportionalResult(name string, numerator int, denominator int, return CheckResult{ Name: name, Pass: false, + Pass2: ResultFail, Confidence: MaxResultConfidence, } } @@ -111,6 +126,7 @@ func MakeProportionalResult(name string, numerator int, denominator int, return CheckResult{ Name: name, Pass: true, + Pass2: ResultPass, Confidence: int(actual * MaxResultConfidence), } } @@ -118,6 +134,7 @@ func MakeProportionalResult(name string, numerator int, denominator int, return CheckResult{ Name: name, Pass: false, + Pass2: ResultFail, Confidence: MaxResultConfidence - int(actual*MaxResultConfidence), } } @@ -140,6 +157,7 @@ func isMinResult(result, min CheckResult) bool { func MakeAndResult(checks ...CheckResult) CheckResult { minResult := CheckResult{ Pass: true, + Pass2: ResultPass, Confidence: MaxResultConfidence, } diff --git a/checker/check_runner.go b/checker/check_runner.go index 9831d96523d..2736907846a 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -44,12 +44,32 @@ type logger struct { messages2 []CheckDetail } -type Logf2 func(typ int, code, desc string, args ...interface{}) +type CheckLogger struct { + l *logger +} + +func (l *CheckLogger) Fail(code, desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailFail, Code: code, Desc: fmt.Sprintf(desc, args...)} + cd.Validate() + l.l.messages2 = append(l.l.messages2, cd) +} + +func (l *CheckLogger) Pass(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailPass, Code: "", Desc: fmt.Sprintf(desc, args...)} + cd.Validate() + l.l.messages2 = append(l.l.messages2, cd) +} + +func (l *CheckLogger) Info(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailInfo, Code: "", Desc: fmt.Sprintf(desc, args...)} + cd.Validate() + l.l.messages2 = append(l.l.messages2, cd) +} -func (l *logger) Logf2(typ int, code, desc string, args ...interface{}) { - cd := CheckDetail{Type: typ, Code: code, Desc: fmt.Sprintf(desc, args...)} +func (l *CheckLogger) Warn(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailWarn, Code: "", Desc: fmt.Sprintf(desc, args...)} cd.Validate() - l.messages2 = append(l.messages2, cd) + l.l.messages2 = append(l.l.messages2, cd) } func (l *logger) Logf(s string, f ...interface{}) { @@ -79,12 +99,14 @@ func (r *Runner) Run(ctx context.Context, f CheckFn) CheckResult { var res CheckResult var l logger + var cl CheckLogger for retriesRemaining := checkRetries; retriesRemaining > 0; retriesRemaining-- { checkRequest := r.CheckRequest checkRequest.Ctx = ctx l = logger{} + cl = CheckLogger{l: &l} checkRequest.Logf = l.Logf - checkRequest.Logf2 = l.Logf2 + checkRequest.CLogger = cl res = f(&checkRequest) if res.ShouldRetry && !strings.Contains(res.Error.Error(), "invalid header field value") { checkRequest.Logf("error, retrying: %s", res.Error) diff --git a/checks/automatic_dependency_update.go b/checks/automatic_dependency_update.go index d0fb301ecec..a19d68e3067 100644 --- a/checks/automatic_dependency_update.go +++ b/checks/automatic_dependency_update.go @@ -20,32 +20,38 @@ import ( "github.com/ossf/scorecard/checker" ) -const CheckAutomaticDependencyUpdate = "Automatic-Dependency-Update" +const checkAutomaticDependencyUpdate = "Automatic-Dependency-Update" //nolint func init() { - registerCheck(CheckAutomaticDependencyUpdate, AutomaticDependencyUpdate) + registerCheck(checkAutomaticDependencyUpdate, AutomaticDependencyUpdate) } // AutomaticDependencyUpdate will check the repository if it contains Automatic dependency update. func AutomaticDependencyUpdate(c *checker.CheckRequest) checker.CheckResult { - result := CheckIfFileExists(CheckAutomaticDependencyUpdate, c, fileExists) - if !result.Pass { - result.Confidence = 3 + r, err := CheckIfFileExists2(checkAutomaticDependencyUpdate, c, fileExists) + if err != nil { + return checker.MakeInconclusiveResult(checkAutomaticDependencyUpdate, err) } - return result + if !r { + c.CLogger.Fail("E01", "no configuration file found in the repo") + return checker.MakeInconclusiveResult(checkAutomaticDependencyUpdate, nil) + } + + // We're confident it's correct. + return checker.MakePassResult(checkAutomaticDependencyUpdate) } // fileExists will validate the if frozen dependencies file name exists. -func fileExists(name string, logf func(s string, f ...interface{})) (bool, error) { +func fileExists(name string, cl checker.CheckLogger) (bool, error) { switch strings.ToLower(name) { case ".github/dependabot.yml": - logf("dependabot config found: %s", name) + cl.Pass("dependabot config found: %s", name) return true, nil // https://docs.renovatebot.com/configuration-options/ case ".github/renovate.json", ".github/renovate.json5", ".renovaterc.json", "renovate.json", "renovate.json5", ".renovaterc": - logf("renovate config found: %s", name) + cl.Pass("renovate config found: %s", name) return true, nil default: return false, nil diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 8df0477b6ca..f524fd9682d 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -26,30 +26,27 @@ import ( //nolint func init() { - registerCheck(checkBinaryArtifactsName, binaryArtifacts) + registerCheck(checkBinaryArtifacts, binaryArtifacts) } -// TODO: read from file? -const ( - checkBinaryArtifactsNumber string = "0001" - checkBinaryArtifactsName string = "Binary-Artifacts" -) +// TODO: read the check code from file? +const checkBinaryArtifacts string = "Binary-Artifacts" // BinaryArtifacts will check the repository if it contains binary artifacts. func binaryArtifacts(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*", false, c, checkBinaryFileContent) if err != nil || !r { // TODO: we're losing the RetryError, should be handled by caller. - return checker.MakeFailResult(checkBinaryArtifactsName, err) + return checker.MakeFailResult(checkBinaryArtifacts, err) } // We're confident it's correct. - c.Logf2(checker.DetailPass, checkBinaryArtifactsNumber, "no binary files found in the repo") - return checker.MakePassResult(checkBinaryArtifactsName) + c.CLogger.Pass("no binary files found in the repo") + return checker.MakePassResult(checkBinaryArtifacts) } func checkBinaryFileContent(path string, content []byte, - l checker.Logf2) (bool, error) { + cl checker.CheckLogger) (bool, error) { binaryFileTypes := map[string]bool{ "crx": true, "deb": true, @@ -94,11 +91,11 @@ func checkBinaryFileContent(path string, content []byte, } if _, ok := binaryFileTypes[t.Extension]; ok { - l(checker.DetailFail, checkBinaryArtifactsNumber+"-E01", "binary-artifact found: %s", path) + cl.Fail("E01", "binary-artifact found: %s", path) return false, nil } else if _, ok := binaryFileTypes[filepath.Ext(path)]; ok { // Falling back to file based extension. - l(checker.DetailFail, checkBinaryArtifactsNumber+"-E01", "binary-artifact found: %s", path) + cl.Fail("E01", "binary-artifact found: %s", path) return false, nil } diff --git a/checks/checkforcontent.go b/checks/checkforcontent.go index a0cf4ee9c44..31fc8eaf345 100644 --- a/checks/checkforcontent.go +++ b/checks/checkforcontent.go @@ -110,7 +110,7 @@ func CheckFilesContent2(shellPathFnPattern string, caseSensitive bool, c *checker.CheckRequest, onFileContent func(path string, content []byte, - l checker.Logf2) (bool, error), + cl checker.CheckLogger) (bool, error), ) (bool, error) { predicate := func(filepath string) bool { // Filter out Scorecard's own test files. @@ -120,7 +120,7 @@ func CheckFilesContent2(shellPathFnPattern string, // Filter out files based on path/names using the pattern. b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive) if err != nil { - c.Logf2(checker.DetailWarning, "4567", "internal error: %v", err) + c.CLogger.Warn("internal error isMatchingPath: %v", err) return false } return b @@ -132,7 +132,7 @@ func CheckFilesContent2(shellPathFnPattern string, return false, err } - rr, err := onFileContent(file, content, c.Logf2) + rr, err := onFileContent(file, content, c.CLogger) if err != nil { return false, err } diff --git a/checks/checkforfile.go b/checks/checkforfile.go index 7de6812e434..d5f031f1e71 100644 --- a/checks/checkforfile.go +++ b/checks/checkforfile.go @@ -44,3 +44,19 @@ func CheckIfFileExists(checkName string, c *checker.CheckRequest, onFile func(na Confidence: confidence, } } + +func CheckIfFileExists2(checkName string, c *checker.CheckRequest, onFile func(name string, + cl checker.CheckLogger) (bool, error)) (bool, error) { + for _, filename := range c.RepoClient.ListFiles(func(string) bool { return true }) { + rr, err := onFile(filename, c.CLogger) + if err != nil { + return false, err + } + + if rr { + return true, nil + } + } + + return false, nil +} diff --git a/checks/checks2.yaml b/checks/checks2.yaml index 1ad70dd730f..0370f814b6d 100644 --- a/checks/checks2.yaml +++ b/checks/checks2.yaml @@ -16,10 +16,10 @@ # Run `cd checks/main && go run /main` to generate `checks.json` and `checks.md`. checks: Binary-Artifacts: - code: C0001 description: >- This check tries to determine if a project has binary artifacts in the source repository. These binaries could be compromised artifacts. Building from the source is recommended. + url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#binary-artifacts remediation: - >- Remove the binary artifacts from the repository. @@ -27,10 +27,31 @@ checks: E01: description: >- A binary file is found in the repository. + url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#binary-artifacts-E01 remediation: - >- Remove the binary from the repository code. - >- Build from the source. + Automatic-Dependency-Update: + description: >- + This check tries to determine if a project has dependencies automatically updated. + The checks looks for [dependabot](https://dependabot.com/docs/config-file/) or + [renovatebot](https://docs.renovatebot.com/configuration-options/). This check only looks if + it is enabled and does not ensure that it is run and pull requests are merged. + url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#automatic-dependency-update + remediation: + - >- + Signup for automatic dependency updates with dependabot or renovatebot and place the config + file in the locations that are recommended by these tools. + errors: + E01: + description: >- + No configuration file found in the repository. + url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#automatic-dependency-update-E01 + remediation: >- + Signup for automatic dependency updates with dependabot or renovatebot and place the config + file in the locations that are recommended by these tools. + diff --git a/checks/code_review.go b/checks/code_review.go index ff5b25d677f..9d6e6883482 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -25,8 +25,8 @@ import ( ) const ( - // CheckCodeReview is the registered name for DoesCodeReview. - CheckCodeReview = "Code-Review" + // checkCodeReview is the registered name for DoesCodeReview. + checkCodeReview = "Code-Review" crPassThreshold = .75 pullRequestsToAnalyze = 30 reviewsToAnalyze = 30 @@ -71,7 +71,7 @@ var ( //nolint:gochecknoinits func init() { - registerCheck(CheckCodeReview, DoesCodeReview) + registerCheck(checkCodeReview, DoesCodeReview) } // DoesCodeReview attempts to determine whether a project requires review before code gets merged. @@ -88,7 +88,7 @@ func DoesCodeReview(c *checker.CheckRequest) checker.CheckResult { "labelsToAnalyze": githubv4.Int(labelsToAnalyze), } if err := c.GraphClient.Query(c.Ctx, &prHistory, vars); err != nil { - return checker.MakeInconclusiveResult(CheckCodeReview, err) + return checker.MakeInconclusiveResult(checkCodeReview, err) } return checker.MultiCheckOr( IsPrReviewRequired, @@ -112,7 +112,7 @@ func GithubCodeReview(c *checker.CheckRequest) checker.CheckResult { foundApprovedReview := false for _, r := range pr.LatestReviews.Nodes { if r.State == "APPROVED" { - c.Logf("found review approved pr: %d", pr.Number) + c.CLogger.Info("found review approved pr: %d", pr.Number) totalReviewed++ foundApprovedReview = true break @@ -124,31 +124,33 @@ func GithubCodeReview(c *checker.CheckRequest) checker.CheckResult { // time on clicking the approve button. if !foundApprovedReview { if !pr.MergeCommit.AuthoredByCommitter { - c.Logf("found pr with committer different than author: %d", pr.Number) + c.CLogger.Info("found pr with committer different than author: %d", pr.Number) totalReviewed++ } } } if totalReviewed > 0 { - c.Logf("github code reviews found") + c.CLogger.Info("github code reviews found for %v commits out of the last %v", totalReviewed, totalMerged) } - return checker.MakeProportionalResult(CheckCodeReview, totalReviewed, totalMerged, crPassThreshold) + return checker.MakeProportionalResult(checkCodeReview, totalReviewed, totalMerged, crPassThreshold) } func IsPrReviewRequired(c *checker.CheckRequest) checker.CheckResult { // Look to see if review is enforced. // Check the branch protection rules, we may not be able to get these though. if prHistory.Repository.DefaultBranchRef.BranchProtectionRule.RequiredApprovingReviewCount >= 1 { - c.Logf("pr review policy enforced") - const confidence = 5 + c.CLogger.Pass("branch protection for default branch is enabled") + // If the default value is 0 when we cannot retrieve the value, + // a non-zero value means we're confident it's enabled. return checker.CheckResult{ - Name: CheckCodeReview, + Name: checkCodeReview, Pass: true, - Confidence: confidence, + Pass2: checker.ResultPass, + Confidence: checker.MaxResultConfidence, } } - return checker.MakeInconclusiveResult(CheckCodeReview, nil) + return checker.MakeInconclusiveResult(checkCodeReview, nil) } func ProwCodeReview(c *checker.CheckRequest) checker.CheckResult { @@ -169,16 +171,17 @@ func ProwCodeReview(c *checker.CheckRequest) checker.CheckResult { } if totalReviewed == 0 { - return checker.MakeInconclusiveResult(CheckCodeReview, ErrorNoReviews) + return checker.MakeInconclusiveResult(checkCodeReview, ErrorNoReviews) } - c.Logf("prow code reviews found") - return checker.MakeProportionalResult(CheckCodeReview, totalReviewed, totalMerged, crPassThreshold) + + c.CLogger.Info("prow code reviews found for %v commits out of the last %v", totalReviewed, totalMerged) + return checker.MakeProportionalResult(checkCodeReview, totalReviewed, totalMerged, crPassThreshold) } func CommitMessageHints(c *checker.CheckRequest) checker.CheckResult { commits, _, err := c.Client.Repositories.ListCommits(c.Ctx, c.Owner, c.Repo, &github.CommitsListOptions{}) if err != nil { - return checker.MakeRetryResult(CheckCodeReview, err) + return checker.MakeRetryResult(checkCodeReview, err) } total := 0 @@ -193,7 +196,7 @@ func CommitMessageHints(c *checker.CheckRequest) checker.CheckResult { } } if isBot { - c.Logf("skip commit from bot account: %s", committer) + c.CLogger.Info("skip commit from bot account: %s", committer) continue } @@ -209,8 +212,10 @@ func CommitMessageHints(c *checker.CheckRequest) checker.CheckResult { } if totalReviewed == 0 { - return checker.MakeInconclusiveResult(CheckCodeReview, ErrorNoReviews) + c.CLogger.Info("none of the %v commit are reviewed via gerrit", total) + return checker.MakeInconclusiveResult(checkCodeReview, ErrorNoReviews) } - c.Logf("code reviews found") - return checker.MakeProportionalResult(CheckCodeReview, totalReviewed, total, crPassThreshold) + + c.CLogger.Info("code reviews found for %v commits out of the last %v", totalReviewed, total) + return checker.MakeProportionalResult(checkCodeReview, totalReviewed, total, crPassThreshold) } diff --git a/cmd/root.go b/cmd/root.go index 98dea859b3a..18780b56623 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -51,6 +51,7 @@ var ( pypi string rubygems string showDetails bool + // TODO(laurent): add explain command. // ErrorInvalidFormatFlag indicates an invalid option was passed for the 'format' argument. ErrorInvalidFormatFlag = errors.New("invalid format flag") ) diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index ef12a9a128b..08622103dee 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -144,3 +144,16 @@ func displayResult(result bool) string { } return "Fail" } + +func displayResult2(result int) string { + switch result { + default: + panic("invalid result") + case checker.ResultPass: + return "Pass" + case checker.ResultFail: + return "Fail" + case checker.ResultDontKnow: + return "N/A" + } +} From 65b918795c88e0596ee62ea20dbfbd53292c3215 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Wed, 7 Jul 2021 20:54:19 +0000 Subject: [PATCH 04/31] comments --- checker/check_request.go | 2 + checker/check_result.go | 155 ++++++++++++++++++++++---- checker/check_runner.go | 23 +++- checks/automatic_dependency_update.go | 11 +- checks/binary_artifact.go | 22 ++-- checks/checkforcontent.go | 2 +- checks/checks2.yaml | 105 +++++++++++++---- checks/code_review.go | 75 +++++++------ checks/frozen_deps.go | 141 +++++++++++++++++------ checks/shell_download_validate.go | 35 +++--- cmd/root.go | 6 +- cmd/serve.go | 2 +- errors/names.go | 12 +- errors/types.go | 10 +- pkg/scorecard_result.go | 5 +- 15 files changed, 446 insertions(+), 160 deletions(-) diff --git a/checker/check_request.go b/checker/check_request.go index bb54e22819b..a9674283e25 100644 --- a/checker/check_request.go +++ b/checker/check_request.go @@ -30,6 +30,8 @@ type CheckRequest struct { GraphClient *githubv4.Client HTTPClient *http.Client RepoClient clients.RepoClient + // Note: Ultimately Log will be removed and replaced by + // CLogger. Logf func(s string, f ...interface{}) CLogger CheckLogger Owner, Repo string diff --git a/checker/check_result.go b/checker/check_result.go index 33480c5df1c..63652536d50 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -16,37 +16,36 @@ package checker import ( "errors" - "fmt" scorecarderrors "github.com/ossf/scorecard/errors" ) -const MaxResultConfidence = 10 +const ( + MaxResultConfidence = 10 + HalfResultConfidence = 5 + MinResultConfidence = 0 +) // ErrorDemoninatorZero indicates the denominator for a proportional result is 0. var ErrorDemoninatorZero = errors.New("internal error: denominator is 0") // Types of details. +type DetailType int + const ( - DetailFail = 0 - DetailPass = 1 - DetailInfo = 2 - DetailWarn = 3 + DetailFail DetailType = iota + DetailPass + DetailInfo + DetailWarn + DetailDebug ) // CheckDetail contains information for each detail. //nolint:govet type CheckDetail struct { - Type int // Any of DetailFail, DetailPass, DetailInfo. - Code string // A 4 digit string identifying the code, e.g. for remediation. - Desc string // A short string representation of the information. -} - -func (cd *CheckDetail) Validate() { - if cd.Type < DetailFail || - cd.Type > DetailWarn { - panic(fmt.Sprintf("invalid CheckDetail type: %v", cd.Type)) - } + Type DetailType // Any of DetailFail, DetailPass, DetailInfo. + Code string // A string identifying the sub-check, e.g. to lookup remediation info. + Desc string // A short string representation of the information. } // Types of results. @@ -57,23 +56,48 @@ const ( ) type CheckResult struct { - Error error `json:"-"` - Name string - Details []string - Details2 []CheckDetail - Confidence int + Error error `json:"-"` + Name string + Details []string + Details2 []CheckDetail + Confidence int + // Note: Pass2 will ultimately be renamed + // as Pass. Pass bool Pass2 int ShouldRetry bool `json:"-"` } +// Will be removed. func MakeInconclusiveResult(name string, err error) CheckResult { return CheckResult{ Name: name, Pass: false, Confidence: 0, Pass2: ResultDontKnow, - Error: scorecarderrors.MakeZeroConfidenceError(err), + Error: scorecarderrors.MakeLowConfidenceError(err), + } +} + +// TODO: these functions should set the details as well. +func MakeInternalErrorResult(name string, err error) CheckResult { + return CheckResult{ + Name: name, + Pass: false, + Confidence: 0, + Pass2: ResultDontKnow, + Error: scorecarderrors.MakeLowConfidenceError(err), + } +} + +func MakeInconclusiveResult2(name string, c *CheckRequest, reason string) CheckResult { + c.CLogger.Warn("lowering result confidence to %d because %s", 0, reason) + return CheckResult{ + Name: name, + Pass: false, + Confidence: 0, + Pass2: ResultDontKnow, + Error: nil, } } @@ -83,6 +107,50 @@ func MakePassResult(name string) CheckResult { Pass: true, Pass2: ResultPass, Confidence: MaxResultConfidence, + Error: nil, + } +} + +func MakePassResultWithHighConfidence(name string) CheckResult { + return CheckResult{ + Name: name, + Pass: true, + Pass2: ResultPass, + Confidence: MaxResultConfidence, + Error: nil, + } +} + +func MakePassResultWithHighConfidenceAndReason(name string, c *CheckRequest, reason string) CheckResult { + c.CLogger.Pass("%s", reason) + return CheckResult{ + Name: name, + Pass: true, + Pass2: ResultPass, + Confidence: MaxResultConfidence, + Error: nil, + } +} + +func MakePassResultWithHighConfidenceAndReasonAndCode(name string, c *CheckRequest, code, reason string) CheckResult { + c.CLogger.PassWithCode(code, reason) + return CheckResult{ + Name: name, + Pass: true, + Pass2: ResultPass, + Confidence: MaxResultConfidence, + Error: nil, + } +} + +func MakePassResultWithLowConfidenceAndReason(name string, c *CheckRequest, conf int, reason string) CheckResult { + c.CLogger.Warn("%s (lowering confidence to %d)", reason, conf) + return CheckResult{ + Name: name, + Pass: true, + Pass2: ResultPass, + Confidence: conf, + Error: nil, } } @@ -96,6 +164,49 @@ func MakeFailResult(name string, err error) CheckResult { } } +func MakeFailResultWithHighConfidence(name string) CheckResult { + return CheckResult{ + Name: name, + Pass: false, + Pass2: ResultFail, + Confidence: MaxResultConfidence, + Error: nil, + } +} + +func MakeFailResultWithHighConfidenceAndReason(name string, c *CheckRequest, reason string) CheckResult { + c.CLogger.Info("%s", reason) + return CheckResult{ + Name: name, + Pass: false, + Pass2: ResultFail, + Confidence: MaxResultConfidence, + Error: nil, + } +} + +func MakeFailResultWithHighConfidenceAndReasonAndCode(name string, c *CheckRequest, code, reason string) CheckResult { + c.CLogger.FailWithCode(code, reason) + return CheckResult{ + Name: name, + Pass: false, + Pass2: ResultFail, + Confidence: MaxResultConfidence, + Error: nil, + } +} + +func MakeFailResultLowConfidenceAndReason(name string, c *CheckRequest, conf int, reason string) CheckResult { + c.CLogger.Fail("%s (lowering confidence to %d)", reason, conf) + return CheckResult{ + Name: name, + Pass: false, + Pass2: ResultFail, + Confidence: conf, + Error: nil, + } +} + func MakeRetryResult(name string, err error) CheckResult { return CheckResult{ Name: name, diff --git a/checker/check_runner.go b/checker/check_runner.go index 2736907846a..fa526eca941 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -40,6 +40,8 @@ type CheckFn func(*CheckRequest) CheckResult type CheckNameToFnMap map[string]CheckFn type logger struct { + // Note: messages2 will ultimately + // be renamed to messages. messages []string messages2 []CheckDetail } @@ -48,27 +50,38 @@ type CheckLogger struct { l *logger } -func (l *CheckLogger) Fail(code, desc string, args ...interface{}) { +func (l *CheckLogger) FailWithCode(code, desc string, args ...interface{}) { cd := CheckDetail{Type: DetailFail, Code: code, Desc: fmt.Sprintf(desc, args...)} - cd.Validate() + l.l.messages2 = append(l.l.messages2, cd) +} + +func (l *CheckLogger) Fail(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailFail, Code: "", Desc: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } func (l *CheckLogger) Pass(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailPass, Code: "", Desc: fmt.Sprintf(desc, args...)} - cd.Validate() + l.l.messages2 = append(l.l.messages2, cd) +} + +func (l *CheckLogger) PassWithCode(code, desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailPass, Code: code, Desc: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } func (l *CheckLogger) Info(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailInfo, Code: "", Desc: fmt.Sprintf(desc, args...)} - cd.Validate() l.l.messages2 = append(l.l.messages2, cd) } func (l *CheckLogger) Warn(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailWarn, Code: "", Desc: fmt.Sprintf(desc, args...)} - cd.Validate() + l.l.messages2 = append(l.l.messages2, cd) +} + +func (l *CheckLogger) Debug(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailDebug, Code: "", Desc: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } diff --git a/checks/automatic_dependency_update.go b/checks/automatic_dependency_update.go index a19d68e3067..79e75189873 100644 --- a/checks/automatic_dependency_update.go +++ b/checks/automatic_dependency_update.go @@ -31,15 +31,16 @@ func init() { func AutomaticDependencyUpdate(c *checker.CheckRequest) checker.CheckResult { r, err := CheckIfFileExists2(checkAutomaticDependencyUpdate, c, fileExists) if err != nil { - return checker.MakeInconclusiveResult(checkAutomaticDependencyUpdate, err) + return checker.MakeInternalErrorResult(checkAutomaticDependencyUpdate, err) } if !r { - c.CLogger.Fail("E01", "no configuration file found in the repo") - return checker.MakeInconclusiveResult(checkAutomaticDependencyUpdate, nil) + return checker.MakeInconclusiveResult2(checkAutomaticDependencyUpdate, c, "no configuration file found in the repo") } - // We're confident it's correct. - return checker.MakePassResult(checkAutomaticDependencyUpdate) + // High confidence result. + // We need not give a reason since it's explained by the calls to + // `cl.Pass` in fileExists. + return checker.MakePassResultWithHighConfidence(checkAutomaticDependencyUpdate) } // fileExists will validate the if frozen dependencies file name exists. diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index f524fd9682d..6c3d4bfc689 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -35,14 +35,20 @@ const checkBinaryArtifacts string = "Binary-Artifacts" // BinaryArtifacts will check the repository if it contains binary artifacts. func binaryArtifacts(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*", false, c, checkBinaryFileContent) - if err != nil || !r { - // TODO: we're losing the RetryError, should be handled by caller. - return checker.MakeFailResult(checkBinaryArtifacts, err) + if err != nil { + // TODO: check for the repo retry error, which should be a common + // scorecard error independent of the underlying implementation. + return checker.MakeInternalErrorResult(checkBinaryArtifacts, err) + } + if !r { + // We need not provid a reason because it's already done + // in checkBinaryFileContent via `Pass` call. + return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) } - // We're confident it's correct. - c.CLogger.Pass("no binary files found in the repo") - return checker.MakePassResult(checkBinaryArtifacts) + // High confidence result. + // We provide a reason to help the user. + return checker.MakePassResultWithHighConfidenceAndReason(checkBinaryArtifacts, c, "no binary files found in the repo") } func checkBinaryFileContent(path string, content []byte, @@ -91,11 +97,11 @@ func checkBinaryFileContent(path string, content []byte, } if _, ok := binaryFileTypes[t.Extension]; ok { - cl.Fail("E01", "binary-artifact found: %s", path) + cl.Fail("binary-artifact found: %s", path) return false, nil } else if _, ok := binaryFileTypes[filepath.Ext(path)]; ok { // Falling back to file based extension. - cl.Fail("E01", "binary-artifact found: %s", path) + cl.Fail("binary-artifact found: %s", path) return false, nil } diff --git a/checks/checkforcontent.go b/checks/checkforcontent.go index 31fc8eaf345..a8967a407c1 100644 --- a/checks/checkforcontent.go +++ b/checks/checkforcontent.go @@ -120,7 +120,7 @@ func CheckFilesContent2(shellPathFnPattern string, // Filter out files based on path/names using the pattern. b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive) if err != nil { - c.CLogger.Warn("internal error isMatchingPath: %v", err) + c.CLogger.Warn("%v", err) return false } return b diff --git a/checks/checks2.yaml b/checks/checks2.yaml index 0370f814b6d..37b895b94fe 100644 --- a/checks/checks2.yaml +++ b/checks/checks2.yaml @@ -19,39 +19,106 @@ checks: description: >- This check tries to determine if a project has binary artifacts in the source repository. These binaries could be compromised artifacts. Building from the source is recommended. - url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#binary-artifacts remediation: - >- Remove the binary artifacts from the repository. - errors: - E01: - description: >- - A binary file is found in the repository. - url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#binary-artifacts-E01 - remediation: - - >- - Remove the binary from the repository code. - - >- - Build from the source. Automatic-Dependency-Update: description: >- This check tries to determine if a project has dependencies automatically updated. The checks looks for [dependabot](https://dependabot.com/docs/config-file/) or [renovatebot](https://docs.renovatebot.com/configuration-options/). This check only looks if it is enabled and does not ensure that it is run and pull requests are merged. - url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#automatic-dependency-update remediation: - >- Signup for automatic dependency updates with dependabot or renovatebot and place the config file in the locations that are recommended by these tools. - errors: - E01: + Code-Review: + description: >- + This check tries to determine if a project requires code review before + pull requests are merged. First it checks if branch-Protection is enabled + on the default branch and the number of reviewers is at least 1. If this + fails, it checks if the recent (~30) commits have a Github-approved + review or if the merger is different from the committer (implicit review). + The check succeeds if at least 75% of commits have a review as described + above. If it fails, it does the same check but looking for reviews by + [Prow](https://github.com/kubernetes/test-infra/tree/master/prow#readme) + (labels "lgtm" or "approved"). If this fails, it does the same but looking + for gerrit-specific commit messages ("Reviewed-on" and "Reviewed-by"). + remediation: + - >- + Follow security best practices by performing strict code reviews for + every new pull request. + - >- + Make "code reviews" mandatory in your repository configuration. E.g. + [GitHub](https://docs.github.com/en/github/administering-a-repository/about-protected-branches#require-pull-request-reviews-before-merging). + - >- + Enforce the rule for administrators / code owners as well. E.g. + [GitHub](https://docs.github.com/en/github/administering-a-repository/about-protected-branches#include-administrators) + Frozen-Deps: + description: >- + This check tries to determine if a project has declared and pinned its + dependencies. It works by (1) looking for the following files in the root + directory: go.mod, go.sum (Golang), package-lock.json, npm-shrinkwrap.json + (Javascript), requirements.txt, pipfile.lock (Python), gemfile.lock + (Ruby), cargo.lock (Rust), yarn.lock (package manager), composer.lock + (PHP), vendor/, third_party/, third-party/; (2) looks for + unpinned dependencies in Dockerfiles, shell scripts and GitHub workflows. If one of + the files in (1) AND all the dependencies in (2) are pinned, the check + succeds. + remediation: + - >- + Declare all your dependencies with specific versions in your package + format file (e.g. `package.json` for npm, `requirements.txt` for + python). For C/C++, check in the code from a trusted source and add a + `README` on the specific version used (and the archive SHA hashes). + - >- + If the package manager supports lock files (e.g. `package-lock.json` for + npm), make sure to check these in the source code as well. These files + maintain signatures for the entire dependency tree and saves from future + exploitation in case the package is compromised. + - >- + For Dockerfiles and github workflows, pin dependencies by hash. See example + [gitcache-docker.yaml](https://github.com/ossf/scorecard/blob/main/.github/workflows/gitcache-docker.yaml#L36) + and [Dockerfile](https://github.com/ossf/scorecard/blob/main/cron/worker/Dockerfile) examples. + - >- + To help update your dependencies after pinning them, use tools such as + Github's [dependabot](https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/) + or [renovate bot](https://github.com/renovatebot/renovate). + failures: + LockFile: description: >- - No configuration file found in the repository. - url: https://github.com/ossf/scorecard/blob/main/checks/checks.md#automatic-dependency-update-E01 - remediation: >- - Signup for automatic dependency updates with dependabot or renovatebot and place the config - file in the locations that are recommended by these tools. + No lock file is found in the root direory of the repo. + remediation: + - >- + Declare all your dependencies with specific versions in your package + format file (e.g. `package.json` for npm, `requirements.txt` for + python). For C/C++, check in the code from a trusted source and add a + `README` on the specific version used (and the archive SHA hashes). + - >- + If the package manager supports lock files (e.g. `package-lock.json` for + npm), make sure to check these in the source code as well. These files + maintain signatures for the entire dependency tree and saves from future + exploitation in case the package is compromised. + GitHubActions: + description: >- + GitHub workflows use non-pinned dependencies. + remediation: + - >- + pin dependencies by hash. See example + [gitcache-docker.yaml](https://github.com/ossf/scorecard/blob/main/.github/workflows/gitcache-docker.yaml#L36) + BinaryDownload: + description: >- + GitHub workflows, Dockerfiles or shell scripts download binaries from the Internet. + remediation: + - >- + Build from source. For shell scripts, commimt to the repo or use [sget](https://blog.sigstore.dev/a-safer-curl-bash-7698c8125063) + and pinn by hash. + Dockerfile: + description: >- + Dockerfile does not pin its dependencies by has in `FROM`. + remediation: + - >- + Pin dependencies by hash. See [Dockerfile](https://github.com/ossf/scorecard/blob/main/cron/worker/Dockerfile) examples. diff --git a/checks/code_review.go b/checks/code_review.go index 9d6e6883482..dffff6a471e 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -16,6 +16,7 @@ package checks import ( "errors" + "fmt" "strings" "github.com/google/go-github/v32/github" @@ -88,17 +89,19 @@ func DoesCodeReview(c *checker.CheckRequest) checker.CheckResult { "labelsToAnalyze": githubv4.Int(labelsToAnalyze), } if err := c.GraphClient.Query(c.Ctx, &prHistory, vars); err != nil { - return checker.MakeInconclusiveResult(checkCodeReview, err) + // Note: this error should not be wrapped. We should + // return a scorecard error instead. + return checker.MakeInternalErrorResult(checkCodeReview, err) } return checker.MultiCheckOr( - IsPrReviewRequired, - GithubCodeReview, - ProwCodeReview, - CommitMessageHints, + isPrReviewRequired, + githubCodeReview, + prowCodeReview, + commitMessageHints, )(c) } -func GithubCodeReview(c *checker.CheckRequest) checker.CheckResult { +func githubCodeReview(c *checker.CheckRequest) checker.CheckResult { // Look at some merged PRs to see if they were reviewed. totalMerged := 0 totalReviewed := 0 @@ -112,7 +115,7 @@ func GithubCodeReview(c *checker.CheckRequest) checker.CheckResult { foundApprovedReview := false for _, r := range pr.LatestReviews.Nodes { if r.State == "APPROVED" { - c.CLogger.Info("found review approved pr: %d", pr.Number) + c.CLogger.Debug("found review approved pr: %d", pr.Number) totalReviewed++ foundApprovedReview = true break @@ -124,36 +127,27 @@ func GithubCodeReview(c *checker.CheckRequest) checker.CheckResult { // time on clicking the approve button. if !foundApprovedReview { if !pr.MergeCommit.AuthoredByCommitter { - c.CLogger.Info("found pr with committer different than author: %d", pr.Number) + c.CLogger.Debug("found pr with committer different than author: %d", pr.Number) totalReviewed++ } } } - if totalReviewed > 0 { - c.CLogger.Info("github code reviews found for %v commits out of the last %v", totalReviewed, totalMerged) - } - return checker.MakeProportionalResult(checkCodeReview, totalReviewed, totalMerged, crPassThreshold) + return createResult(c, "GitHub", totalReviewed, totalMerged) } -func IsPrReviewRequired(c *checker.CheckRequest) checker.CheckResult { +func isPrReviewRequired(c *checker.CheckRequest) checker.CheckResult { // Look to see if review is enforced. // Check the branch protection rules, we may not be able to get these though. if prHistory.Repository.DefaultBranchRef.BranchProtectionRule.RequiredApprovingReviewCount >= 1 { - c.CLogger.Pass("branch protection for default branch is enabled") // If the default value is 0 when we cannot retrieve the value, // a non-zero value means we're confident it's enabled. - return checker.CheckResult{ - Name: checkCodeReview, - Pass: true, - Pass2: checker.ResultPass, - Confidence: checker.MaxResultConfidence, - } + return checker.MakePassResultWithHighConfidenceAndReason(checkCodeReview, c, "branch protection for default branch is enabled") } - return checker.MakeInconclusiveResult(checkCodeReview, nil) + return checker.MakeInconclusiveResult2(checkCodeReview, c, "not sure if branch protection is enabled") } -func ProwCodeReview(c *checker.CheckRequest) checker.CheckResult { +func prowCodeReview(c *checker.CheckRequest) checker.CheckResult { // Look at some merged PRs to see if they were reviewed totalMerged := 0 totalReviewed := 0 @@ -170,15 +164,10 @@ func ProwCodeReview(c *checker.CheckRequest) checker.CheckResult { } } - if totalReviewed == 0 { - return checker.MakeInconclusiveResult(checkCodeReview, ErrorNoReviews) - } - - c.CLogger.Info("prow code reviews found for %v commits out of the last %v", totalReviewed, totalMerged) - return checker.MakeProportionalResult(checkCodeReview, totalReviewed, totalMerged, crPassThreshold) + return createResult(c, "Prow", totalReviewed, totalMerged) } -func CommitMessageHints(c *checker.CheckRequest) checker.CheckResult { +func commitMessageHints(c *checker.CheckRequest) checker.CheckResult { commits, _, err := c.Client.Repositories.ListCommits(c.Ctx, c.Owner, c.Repo, &github.CommitsListOptions{}) if err != nil { return checker.MakeRetryResult(checkCodeReview, err) @@ -196,7 +185,7 @@ func CommitMessageHints(c *checker.CheckRequest) checker.CheckResult { } } if isBot { - c.CLogger.Info("skip commit from bot account: %s", committer) + c.CLogger.Debug("skip commit from bot account: %s", committer) continue } @@ -211,11 +200,27 @@ func CommitMessageHints(c *checker.CheckRequest) checker.CheckResult { } } - if totalReviewed == 0 { - c.CLogger.Info("none of the %v commit are reviewed via gerrit", total) - return checker.MakeInconclusiveResult(checkCodeReview, ErrorNoReviews) + if totalReviewed > 0 { + reason := fmt.Sprintf("Gerrit code reviews found for %v commits out of the last %v", totalReviewed, total) + if total == totalReviewed { + return checker.MakePassResultWithHighConfidenceAndReason(checkCodeReview, c, reason) + } else { + return checker.MakeFailResultLowConfidenceAndReason(checkCodeReview, c, checker.HalfResultConfidence, reason) + } + } + + return createResult(c, "Gerrit", totalReviewed, total) +} + +func createResult(c *checker.CheckRequest, reviewName string, reviewed, total int) checker.CheckResult { + if reviewed > 0 { + reason := fmt.Sprintf("%s code reviews found for %v commits out of the last %v", reviewName, reviewed, total) + if total == reviewed { + return checker.MakePassResultWithHighConfidenceAndReason(checkCodeReview, c, reason) + } else { + return checker.MakeFailResultLowConfidenceAndReason(checkCodeReview, c, checker.HalfResultConfidence, reason) + } } - c.CLogger.Info("code reviews found for %v commits out of the last %v", totalReviewed, total) - return checker.MakeProportionalResult(checkCodeReview, totalReviewed, total, crPassThreshold) + return checker.MakeInconclusiveResult2(checkCodeReview, c, fmt.Sprintf("no %s reviews found", reviewName)) } diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index 30c2ecfc6cd..76a1e2743fb 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -26,8 +26,8 @@ import ( "github.com/ossf/scorecard/checker" ) -// CheckFrozenDeps is the registered name for FrozenDeps. -const CheckFrozenDeps = "Frozen-Deps" +// checkFrozenDeps is the registered name for FrozenDeps. +const checkFrozenDeps = "Frozen-Deps" // ErrInvalidDockerfile : Invalid docker file. var ErrInvalidDockerfile = errors.New("invalid docker file") @@ -60,7 +60,7 @@ type gitHubActionWorkflowConfig struct { //nolint:gochecknoinits func init() { - registerCheck(CheckFrozenDeps, FrozenDeps) + registerCheck(checkFrozenDeps, FrozenDeps) } // FrozenDeps will check the repository if it contains frozen dependecies. @@ -78,24 +78,50 @@ func FrozenDeps(c *checker.CheckRequest) checker.CheckResult { // TODO(laurent): need to support GCB pinning. func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { - return CheckFilesContent(CheckFrozenDeps, "*", false, c, validateShellScriptDownloads) + r, err := CheckFilesContent2("*", false, c, validateShellScriptDownloads) + if err != nil { + // TODO: check for the repo retry error, which should be a common + // scorecard error independent of the underlying implementation. + return checker.MakeInternalErrorResult(checkFrozenDeps, err) + } + if !r { + // We need not provide a reason/code because it's already done + // in validateDockerfile via `Fail` call. + return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + } + + return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, + "BinaryDownload", "no binary downloads found in shell scripts") } func validateShellScriptDownloads(pathfn string, content []byte, - logf func(s string, f ...interface{})) (bool, error) { + cl checker.CheckLogger) (bool, error) { // Validate the file type. if !isShellScriptFile(pathfn, content) { return true, nil } - return validateShellFile(pathfn, content, logf) + return validateShellFile(pathfn, content, cl) } func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { - return CheckFilesContent(CheckFrozenDeps, "*Dockerfile*", false, c, validateDockerfileDownloads) + r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfileDownloads) + if err != nil { + // TODO: check for the repo retry error, which should be a common + // scorecard error independent of the underlying implementation. + return checker.MakeInternalErrorResult(checkFrozenDeps, err) + } + if !r { + // We need not provide a reason/code because it's already done + // in validateDockerfile via `Fail` call. + return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + } + + return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, + "BinaryDownload", "no binary downloads found in Dockerfiles") } func validateDockerfileDownloads(pathfn string, content []byte, - logf func(s string, f ...interface{})) (bool, error) { + cl checker.CheckLogger) (bool, error) { contentReader := strings.NewReader(string(content)) res, err := parser.Parse(contentReader) if err != nil { @@ -127,15 +153,28 @@ func validateDockerfileDownloads(pathfn string, content []byte, bytes = append(bytes, cmd...) bytes = append(bytes, '\n') } - return validateShellFile(pathfn, bytes, logf) + return validateShellFile(pathfn, bytes, cl) } func isDockerfilePinned(c *checker.CheckRequest) checker.CheckResult { - return CheckFilesContent(CheckFrozenDeps, "*Dockerfile*", false, c, validateDockerfile) + r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfile) + if err != nil { + // TODO: check for the repo retry error, which should be a common + // scorecard error independent of the underlying implementation. + return checker.MakeInternalErrorResult(checkFrozenDeps, err) + } + if !r { + // We need not provide a reason/code because it's already done + // in validateDockerfile via `Fail` call. + return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + } + + return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, + "Dockerfile", "Dockerfile dependencies are pinned") } func validateDockerfile(pathfn string, content []byte, - logf func(s string, f ...interface{})) (bool, error) { + cl checker.CheckLogger) (bool, error) { // Users may use various names, e.g., // Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template // Templates may trigger false positives, e.g. FROM { NAME }. @@ -188,14 +227,14 @@ func validateDockerfile(pathfn string, content []byte, // Not pinned. ret = false - logf("!! frozen-deps/docker - %v has non-pinned dependency '%v'", pathfn, name) + cl.FailWithCode("Dockerfile", "%v has non-pinned dependency '%v'", pathfn, name) // FROM name. case len(valueList) == 1: name := valueList[0] if !regex.Match([]byte(name)) { ret = false - logf("!! frozen-deps/docker - %v has non-pinned dependency '%v'", pathfn, name) + cl.FailWithCode("Dockerfile", "%v has non-pinned dependency '%v'", pathfn, name) } default: @@ -213,11 +252,24 @@ func validateDockerfile(pathfn string, content []byte, } func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { - return CheckFilesContent(CheckFrozenDeps, ".github/workflows/*", false, c, validateGitHubWorkflowShellScriptDownloads) + r, err := CheckFilesContent2(".github/workflows/*", false, c, validateGitHubWorkflowShellScriptDownloads) + if err != nil { + // TODO: check for the repo retry error, which should be a common + // scorecard error independent of the underlying implementation. + return checker.MakeInternalErrorResult(checkFrozenDeps, err) + } + if !r { + // We need not provide a reason/code because it's already done + // in validateGitHubWorkflowShellScriptDownloads via `Fail` call. + return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + } + + return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, + "BinaryDownload", "no binary download found in GitHub workflows") } func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, - logf func(s string, f ...interface{})) (bool, error) { + cl checker.CheckLogger) (bool, error) { if len(content) == 0 { return false, ErrEmptyFile } @@ -225,7 +277,7 @@ func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, var workflow gitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { - return false, fmt.Errorf("!! frozen-deps - cannot unmarshal file %v\n%v: %w", pathfn, string(content), err) + return false, fmt.Errorf("cannot unmarshal file %v\n%v: %w", pathfn, string(content), err) } githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`) @@ -262,7 +314,7 @@ func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, } if scriptContent != "" { - validated, err = validateShellFile(pathfn, []byte(scriptContent), logf) + validated, err = validateShellFile(pathfn, []byte(scriptContent), cl) if err != nil { return false, err } @@ -273,11 +325,26 @@ func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, // Check pinning of github actions in workflows. func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) checker.CheckResult { - return CheckFilesContent(CheckFrozenDeps, ".github/workflows/*", true, c, validateGitHubActionWorkflow) + r, err := CheckFilesContent2(".github/workflows/*", true, c, validateGitHubActionWorkflow) + if err != nil { + // TODO: check for the repo retry error, which should be a common + // scorecard error independent of the underlying implementation. + return checker.MakeInternalErrorResult(checkFrozenDeps, err) + } + if !r { + // We need not provide a reason/code because it's already done + // in validateGitHubActionWorkflow via `Fail` call. + return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + } + + // High confidence result. + // We provide a reason to help the user. + return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, + "GitHubActions", "GitHub actions' dependencies are pinned") } // Check file content. -func validateGitHubActionWorkflow(pathfn string, content []byte, logf func(s string, f ...interface{})) (bool, error) { +func validateGitHubActionWorkflow(pathfn string, content []byte, cl checker.CheckLogger) (bool, error) { if len(content) == 0 { return false, ErrEmptyFile } @@ -285,7 +352,7 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, logf func(s str var workflow gitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { - return false, fmt.Errorf("!! frozen-deps - cannot unmarshal file %v\n%v: %w", pathfn, string(content), err) + return false, fmt.Errorf("cannot unmarshal file %v\n%v: %w", pathfn, string(content), err) } hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`) @@ -301,7 +368,7 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, logf func(s str match := hashRegex.Match([]byte(step.Uses)) if !match { ret = false - logf("!! frozen-deps/github-actions - %v has non-pinned dependency '%v' (job '%v')", pathfn, step.Uses, jobName) + cl.FailWithCode("GitHubActions", "%v has non-pinned dependency '%v' (job '%v')", pathfn, step.Uses, jobName) } } } @@ -312,38 +379,50 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, logf func(s str // Check presence of lock files thru validatePackageManagerFile(). func isPackageManagerLockFilePresent(c *checker.CheckRequest) checker.CheckResult { - return CheckIfFileExists(CheckFrozenDeps, c, validatePackageManagerFile) + r, err := CheckIfFileExists2(checkFrozenDeps, c, validatePackageManagerFile) + if err != nil { + return checker.MakeInternalErrorResult(checkAutomaticDependencyUpdate, err) + } + if !r { + return checker.MakeFailResultWithHighConfidenceAndReasonAndCode(checkAutomaticDependencyUpdate, c, + "LockFile", "no lock file found in the repo") + } + + // High confidence result. + // We don't pass a `reason` because it's already done + // thru calls to `Pass` in validatePackageManagerFile. + return checker.MakePassResultWithHighConfidence(checkAutomaticDependencyUpdate) } // validatePackageManagerFile will validate the if frozen dependecies file name exists. // TODO(laurent): need to differentiate between libraries and programs. // TODO(laurent): handle multi-language repos. -func validatePackageManagerFile(name string, logf func(s string, f ...interface{})) (bool, error) { +func validatePackageManagerFile(name string, cl checker.CheckLogger) (bool, error) { switch strings.ToLower(name) { case "go.mod", "go.sum": - logf("go modules found: %s", name) + cl.PassWithCode("LockFile", "go modules found: %s", name) return true, nil case "vendor/", "third_party/", "third-party/": - logf("vendor dir found: %s", name) + cl.PassWithCode("LockFile", "vendor dir found: %s", name) return true, nil case "package-lock.json", "npm-shrinkwrap.json": - logf("nodejs packages found: %s", name) + cl.PassWithCode("LockFile", "nodejs packages found: %s", name) return true, nil // TODO(laurent): add check for hashbased pinning in requirements.txt - https://davidwalsh.name/hashin case "requirements.txt", "pipfile.lock": - logf("python requirements found: %s", name) + cl.PassWithCode("LockFile", "python requirements found: %s", name) return true, nil case "gemfile.lock": - logf("ruby gems found: %s", name) + cl.PassWithCode("LockFile", "ruby gems found: %s", name) return true, nil case "cargo.lock": - logf("rust crates found: %s", name) + cl.PassWithCode("LockFile", "rust crates found: %s", name) return true, nil case "yarn.lock": - logf("yarn packages found: %s", name) + cl.PassWithCode("LockFile", "yarn packages found: %s", name) return true, nil case "composer.lock": - logf("composer packages found: %s", name) + cl.PassWithCode("LockFile", "composer packages found: %s", name) return true, nil default: return false, nil diff --git a/checks/shell_download_validate.go b/checks/shell_download_validate.go index 064eb2697be..a2674dfd5e3 100644 --- a/checks/shell_download_validate.go +++ b/checks/shell_download_validate.go @@ -25,6 +25,7 @@ import ( "regexp" "strings" + "github.com/ossf/scorecard/checker" "mvdan.cc/sh/v3/syntax" ) @@ -266,7 +267,7 @@ func extractCommand(cmd interface{}) ([]string, bool) { } func isFetchPipeExecute(node syntax.Node, cmd, pathfn string, - logf func(s string, f ...interface{})) bool { + cl checker.CheckLogger) bool { // BinaryCmd {Op=|, X=CallExpr{Args={curl, -s, url}}, Y=CallExpr{Args={bash,}}}. bc, ok := node.(*syntax.BinaryCmd) if !ok { @@ -295,7 +296,7 @@ func isFetchPipeExecute(node syntax.Node, cmd, pathfn string, return false } - logf("!! frozen-deps/fetch-execute - %v is fetching and executing non-pinned program '%v'", + cl.FailWithCode("BinaryDownload", "%v is fetching and executing non-pinned program '%v'", pathfn, cmd) return true } @@ -322,7 +323,7 @@ func getRedirectFile(red []*syntax.Redirect) (string, bool) { } func isExecuteFiles(node syntax.Node, cmd, pathfn string, files map[string]bool, - logf func(s string, f ...interface{})) bool { + cl checker.CheckLogger) bool { ce, ok := node.(*syntax.CallExpr) if !ok { return false @@ -336,7 +337,7 @@ func isExecuteFiles(node syntax.Node, cmd, pathfn string, files map[string]bool, ok = false for fn := range files { if isInterpreterWithFile(c, fn) || isExecuteFile(c, fn) { - logf("!! frozen-deps/fetch-execute - %v is fetching and executing non-pinned program '%v'", + cl.FailWithCode("BinaryDownload", "%v is fetching and executing non-pinned program '%v'", pathfn, cmd) ok = true } @@ -479,7 +480,7 @@ func isPipUnpinnedDownload(cmd []string) bool { } func isUnpinnedPakageManagerDownload(node syntax.Node, cmd, pathfn string, - logf func(s string, f ...interface{})) bool { + cl checker.CheckLogger) bool { ce, ok := node.(*syntax.CallExpr) if !ok { return false @@ -492,14 +493,14 @@ func isUnpinnedPakageManagerDownload(node syntax.Node, cmd, pathfn string, // Go get/install. if isGoUnpinnedDownload(c) { - logf("!! frozen-deps/fetch-execute - %v is fetching an non-pinned dependency '%v'", + cl.FailWithCode("BinaryDownload", "%v is fetching an non-pinned dependency '%v'", pathfn, cmd) return true } // Pip install. if isPipUnpinnedDownload(c) { - logf("!! frozen-deps/fetch-execute - %v is fetching an non-pinned dependency '%v'", + cl.FailWithCode("BBinaryDownload", "%v is fetching an non-pinned dependency '%v'", pathfn, cmd) return true } @@ -533,7 +534,7 @@ func recordFetchFileFromNode(node syntax.Node) (pathfn string, ok bool, err erro } func isFetchProcSubsExecute(node syntax.Node, cmd, pathfn string, - logf func(s string, f ...interface{})) bool { + cl checker.CheckLogger) bool { ce, ok := node.(*syntax.CallExpr) if !ok { return false @@ -583,7 +584,7 @@ func isFetchProcSubsExecute(node syntax.Node, cmd, pathfn string, return false } - logf("!! frozen-deps/fetch-execute - %v is fetching and executing non-pinned program '%v'", + cl.FailWithCode("BinaryDownload", "%v is fetching and executing non-pinned program '%v'", pathfn, cmd) return true } @@ -661,7 +662,7 @@ func nodeToString(p *syntax.Printer, node syntax.Node) (string, error) { } func validateShellFileAndRecord(pathfn string, content []byte, files map[string]bool, - logf func(s string, f ...interface{})) (bool, error) { + cl checker.CheckLogger) (bool, error) { in := strings.NewReader(string(content)) f, err := syntax.NewParser().Parse(in, "") if err != nil { @@ -682,7 +683,7 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string] c, ok := extractInterpreterCommandFromNode(node) // nolinter if ok { - ok, e := validateShellFileAndRecord(pathfn, []byte(c), files, logf) + ok, e := validateShellFileAndRecord(pathfn, []byte(c), files, cl) validated = ok if e != nil { err = e @@ -691,23 +692,23 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string] } // `curl | bash` (supports `sudo`). - if isFetchPipeExecute(node, cmdStr, pathfn, logf) { + if isFetchPipeExecute(node, cmdStr, pathfn, cl) { validated = false } // Check if we're calling a file we previously downloaded. // Includes `curl > /tmp/file [&&|;] [bash] /tmp/file` - if isExecuteFiles(node, cmdStr, pathfn, files, logf) { + if isExecuteFiles(node, cmdStr, pathfn, files, cl) { validated = false } // `bash <(wget -qO- http://website.com/my-script.sh)`. (supports `sudo`). - if isFetchProcSubsExecute(node, cmdStr, pathfn, logf) { + if isFetchProcSubsExecute(node, cmdStr, pathfn, cl) { validated = false } // Package manager's unpinned installs. - if isUnpinnedPakageManagerDownload(node, cmdStr, pathfn, logf) { + if isUnpinnedPakageManagerDownload(node, cmdStr, pathfn, cl) { validated = false } // TODO(laurent): add check for cat file | bash. @@ -783,7 +784,7 @@ func isShellScriptFile(pathfn string, content []byte) bool { return false } -func validateShellFile(pathfn string, content []byte, logf func(s string, f ...interface{})) (bool, error) { +func validateShellFile(pathfn string, content []byte, cl checker.CheckLogger) (bool, error) { files := make(map[string]bool) - return validateShellFileAndRecord(pathfn, content, files, logf) + return validateShellFileAndRecord(pathfn, content, files, cl) } diff --git a/cmd/root.go b/cmd/root.go index 18780b56623..1a09a886854 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -158,11 +158,11 @@ or ./scorecard --{npm,pypi,rubgems}= [--checks=check1,...] [--show switch format { case formatDefault: - err = repoResult.AsString(showDetails, os.Stdout) + err = repoResult.AsString(showDetails, *logLevel, os.Stdout) case formatCSV: - err = repoResult.AsCSV(showDetails, os.Stdout) + err = repoResult.AsCSV(showDetails, *logLevel, os.Stdout) case formatJSON: - err = repoResult.AsJSON(showDetails, os.Stdout) + err = repoResult.AsJSON(showDetails, *logLevel, os.Stdout) default: err = fmt.Errorf("%w %s. allowed values are: [default, csv, json]", ErrorInvalidFormatFlag, format) } diff --git a/cmd/serve.go b/cmd/serve.go index 42917f1aaaf..8d5cb02feda 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -85,7 +85,7 @@ var serveCmd = &cobra.Command{ } if r.Header.Get("Content-Type") == "application/json" { - if err := repoResult.AsJSON(showDetails, rw); err != nil { + if err := repoResult.AsJSON(showDetails, *logLevel, rw); err != nil { sugar.Error(err) rw.WriteHeader(http.StatusInternalServerError) } diff --git a/errors/names.go b/errors/names.go index b78162b5ccc..81df9e14c46 100644 --- a/errors/names.go +++ b/errors/names.go @@ -21,23 +21,23 @@ import ( const ( // RetryError occurs when checks fail after exhausting all retry attempts. RetryError = "RetryError" - // ZeroConfidenceError shows an inconclusive result. - ZeroConfidenceError = "ZeroConfidenceError" + // LowConfidenceError shows a low-confidence result. + LowConfidenceError = "LowConfidenceError" // UnknownError for all error types not handled. UnknownError = "UnknownError" ) var ( - errRetry *ErrRetry - errZeroConfidence *ErrZeroConfidence + errRetry *ErrRetry + errLowConfidence *ErrLowConfidence ) func GetErrorName(err error) string { switch { case errors.As(err, &errRetry): return RetryError - case errors.As(err, &errZeroConfidence): - return ZeroConfidenceError + case errors.As(err, &errLowConfidence): + return LowConfidenceError default: return UnknownError } diff --git a/errors/types.go b/errors/types.go index 421324b3d15..414102cfbd2 100644 --- a/errors/types.go +++ b/errors/types.go @@ -19,8 +19,8 @@ import ( ) type ( - ErrRetry struct{ wrappedError } - ErrZeroConfidence struct{ wrappedError } + ErrRetry struct{ wrappedError } + ErrLowConfidence struct{ wrappedError } ) func MakeRetryError(err error) error { @@ -32,10 +32,10 @@ func MakeRetryError(err error) error { } } -func MakeZeroConfidenceError(err error) error { - return &ErrZeroConfidence{ +func MakeLowConfidenceError(err error) error { + return &ErrLowConfidence{ wrappedError{ - msg: "check result was unknown", + msg: "low confidence check result", innerError: err, }, } diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index 08622103dee..c6c176a7d83 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -27,6 +27,7 @@ import ( "github.com/olekukonko/tablewriter" "github.com/ossf/scorecard/checker" + "go.uber.org/zap/zapcore" ) type ScorecardResult struct { @@ -38,7 +39,7 @@ type ScorecardResult struct { // AsJSON outputs the result in JSON format with a newline at the end. // If called on []ScorecardResult will create NDJson formatted output. -func (r *ScorecardResult) AsJSON(showDetails bool, writer io.Writer) error { +func (r *ScorecardResult) AsJSON(showDetails bool, logLevel zapcore.Level, writer io.Writer) error { encoder := json.NewEncoder(writer) if showDetails { if err := encoder.Encode(r); err != nil { @@ -65,7 +66,7 @@ func (r *ScorecardResult) AsJSON(showDetails bool, writer io.Writer) error { return nil } -func (r *ScorecardResult) AsCSV(showDetails bool, writer io.Writer) error { +func (r *ScorecardResult) AsCSV(showDetails bool, logLevel zapcore.Level, writer io.Writer) error { w := csv.NewWriter(writer) record := []string{r.Repo} columns := []string{"Repository"} From baf45c8f0ac699f92f24b1b8dc4cce2effe577ad Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Wed, 7 Jul 2021 21:40:16 +0000 Subject: [PATCH 05/31] comments --- checker/check_result.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/check_result.go b/checker/check_result.go index 63652536d50..7bb40afedd4 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -59,7 +59,7 @@ type CheckResult struct { Error error `json:"-"` Name string Details []string - Details2 []CheckDetail + Details2 []CheckDetail json:"-" Confidence int // Note: Pass2 will ultimately be renamed // as Pass. @@ -154,6 +154,7 @@ func MakePassResultWithLowConfidenceAndReason(name string, c *CheckRequest, conf } } +// Will be removed. func MakeFailResult(name string, err error) CheckResult { return CheckResult{ Name: name, From 640fd19faf2f7fd40f27a5cf54974623aba136f0 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Wed, 7 Jul 2021 21:43:54 +0000 Subject: [PATCH 06/31] more functions --- checker/check_runner.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/checker/check_runner.go b/checker/check_runner.go index fa526eca941..06a7d705f98 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -75,16 +75,31 @@ func (l *CheckLogger) Info(desc string, args ...interface{}) { l.l.messages2 = append(l.l.messages2, cd) } +func (l *CheckLogger) InfoWithCode(code, desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailInfo, Code: code, Desc: fmt.Sprintf(desc, args...)} + l.l.messages2 = append(l.l.messages2, cd) +} + func (l *CheckLogger) Warn(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailWarn, Code: "", Desc: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } +func (l *CheckLogger) WarnWithCode(code, desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailWarn, Code: code, Desc: fmt.Sprintf(desc, args...)} + l.l.messages2 = append(l.l.messages2, cd) +} + func (l *CheckLogger) Debug(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailDebug, Code: "", Desc: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } +func (l *CheckLogger) DebugWithCode(code, desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailDebug, Code: code, Desc: fmt.Sprintf(desc, args...)} + l.l.messages2 = append(l.l.messages2, cd) +} + func (l *logger) Logf(s string, f ...interface{}) { l.messages = append(l.messages, fmt.Sprintf(s, f...)) } From d22a7c8f423c6609e97ba1d91bc2b7d2fab3b27e Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Wed, 7 Jul 2021 21:44:35 +0000 Subject: [PATCH 07/31] fix --- checks/code_review.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checks/code_review.go b/checks/code_review.go index dffff6a471e..419c423c60e 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -144,7 +144,7 @@ func isPrReviewRequired(c *checker.CheckRequest) checker.CheckResult { // a non-zero value means we're confident it's enabled. return checker.MakePassResultWithHighConfidenceAndReason(checkCodeReview, c, "branch protection for default branch is enabled") } - return checker.MakeInconclusiveResult2(checkCodeReview, c, "not sure if branch protection is enabled") + return checker.MakeInconclusiveResult2(checkCodeReview, c, "cannot determine if branch protection is enabled") } func prowCodeReview(c *checker.CheckRequest) checker.CheckResult { From 1e2a74671c7c3b1e0581bba61f8899fcdec4f1dc Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Wed, 7 Jul 2021 21:45:47 +0000 Subject: [PATCH 08/31] fix --- checker/check_result.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/check_result.go b/checker/check_result.go index 7bb40afedd4..a9a41642f44 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -59,7 +59,7 @@ type CheckResult struct { Error error `json:"-"` Name string Details []string - Details2 []CheckDetail json:"-" + Details2 []CheckDetail `json:"-"` Confidence int // Note: Pass2 will ultimately be renamed // as Pass. From e699839f4208a7250cd93dd85bfcb6a84fd62202 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Thu, 8 Jul 2021 16:47:32 +0000 Subject: [PATCH 09/31] fix --- checks/frozen_deps.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index 76a1e2743fb..d0c0bd3548b 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -381,17 +381,17 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, cl checker.Chec func isPackageManagerLockFilePresent(c *checker.CheckRequest) checker.CheckResult { r, err := CheckIfFileExists2(checkFrozenDeps, c, validatePackageManagerFile) if err != nil { - return checker.MakeInternalErrorResult(checkAutomaticDependencyUpdate, err) + return checker.MakeInternalErrorResult(checkFrozenDeps, err) } if !r { - return checker.MakeFailResultWithHighConfidenceAndReasonAndCode(checkAutomaticDependencyUpdate, c, + return checker.MakeFailResultWithHighConfidenceAndReasonAndCode(checkFrozenDeps, c, "LockFile", "no lock file found in the repo") } // High confidence result. // We don't pass a `reason` because it's already done // thru calls to `Pass` in validatePackageManagerFile. - return checker.MakePassResultWithHighConfidence(checkAutomaticDependencyUpdate) + return checker.MakePassResultWithHighConfidence(checkFrozenDeps) } // validatePackageManagerFile will validate the if frozen dependecies file name exists. From b6c810c3ee37748ad71955feb07645bbd01f3165 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 9 Jul 2021 16:51:03 +0000 Subject: [PATCH 10/31] fixes --- checks/checks2.yaml | 11 +++++++---- checks/shell_download_validate.go | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/checks/checks2.yaml b/checks/checks2.yaml index 37b895b94fe..e06809efdcd 100644 --- a/checks/checks2.yaml +++ b/checks/checks2.yaml @@ -112,13 +112,16 @@ checks: remediation: - >- Build from source. For shell scripts, commimt to the repo or use [sget](https://blog.sigstore.dev/a-safer-curl-bash-7698c8125063) - and pinn by hash. + and pin by hash. Dockerfile: description: >- Dockerfile does not pin its dependencies by has in `FROM`. remediation: - >- Pin dependencies by hash. See [Dockerfile](https://github.com/ossf/scorecard/blob/main/cron/worker/Dockerfile) examples. - - - + PackageInstall: + description: >- + Package managers should command should pin packages they install. + remediation: + - >- + For golang, `go install pkg@hash`. For an example, see [TODO]() diff --git a/checks/shell_download_validate.go b/checks/shell_download_validate.go index a2674dfd5e3..ec549aa41ce 100644 --- a/checks/shell_download_validate.go +++ b/checks/shell_download_validate.go @@ -35,6 +35,11 @@ var ErrParsingDockerfile = errors.New("file cannot be parsed") // ErrParsingShellCommand indicates a problem parsing a shell command. var ErrParsingShellCommand = errors.New("shell command cannot be parsed") +const ( + binaryDownload = "BinaryDownload" + packageInstall = "PackageInstall" +) + // List of interpreters. var pythonInterpreters = []string{"python", "python3", "python2.7"} @@ -296,7 +301,7 @@ func isFetchPipeExecute(node syntax.Node, cmd, pathfn string, return false } - cl.FailWithCode("BinaryDownload", "%v is fetching and executing non-pinned program '%v'", + cl.FailWithCode(binaryDownload, "%v is fetching and executing non-pinned program '%v'", pathfn, cmd) return true } @@ -337,7 +342,7 @@ func isExecuteFiles(node syntax.Node, cmd, pathfn string, files map[string]bool, ok = false for fn := range files { if isInterpreterWithFile(c, fn) || isExecuteFile(c, fn) { - cl.FailWithCode("BinaryDownload", "%v is fetching and executing non-pinned program '%v'", + cl.FailWithCode(binaryDownload, "%v is fetching and executing non-pinned program '%v'", pathfn, cmd) ok = true } @@ -493,14 +498,14 @@ func isUnpinnedPakageManagerDownload(node syntax.Node, cmd, pathfn string, // Go get/install. if isGoUnpinnedDownload(c) { - cl.FailWithCode("BinaryDownload", "%v is fetching an non-pinned dependency '%v'", + cl.FailWithCode(packageInstall, "%v is fetching an non-pinned dependency '%v'", pathfn, cmd) return true } // Pip install. if isPipUnpinnedDownload(c) { - cl.FailWithCode("BBinaryDownload", "%v is fetching an non-pinned dependency '%v'", + cl.FailWithCode(packageInstall, "%v is fetching an non-pinned dependency '%v'", pathfn, cmd) return true } @@ -584,7 +589,7 @@ func isFetchProcSubsExecute(node syntax.Node, cmd, pathfn string, return false } - cl.FailWithCode("BinaryDownload", "%v is fetching and executing non-pinned program '%v'", + cl.FailWithCode(binaryDownload, "%v is fetching and executing non-pinned program '%v'", pathfn, cmd) return true } @@ -786,5 +791,7 @@ func isShellScriptFile(pathfn string, content []byte) bool { func validateShellFile(pathfn string, content []byte, cl checker.CheckLogger) (bool, error) { files := make(map[string]bool) + // TODO(laurent): add pass here for both BinaryDownload and packageInstall + // and remove from caller. return validateShellFileAndRecord(pathfn, content, files, cl) } From cfef928d4f8325cf565fadca01752efc24a4dbb5 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Thu, 15 Jul 2021 18:28:28 +0000 Subject: [PATCH 11/31] offline discussions --- checker/check_request.go | 2 +- checker/check_result.go | 255 +++++++++++++------------- checker/check_runner.go | 107 ++++++----- checks/automatic_dependency_update.go | 16 +- checks/binary_artifact.go | 21 +-- checks/checkforcontent.go | 16 +- checks/checkforfile.go | 4 +- checks/code_review.go | 36 ++-- checks/frozen_deps.go | 145 ++++++--------- checks/permissions.go | 3 +- checks/shell_download_validate.go | 64 +++---- cmd/root.go | 10 +- errors/internal.go | 28 +++ errors/public.go | 38 ++++ pkg/scorecard_result.go | 116 ++++++++++-- 15 files changed, 477 insertions(+), 384 deletions(-) create mode 100644 errors/internal.go create mode 100644 errors/public.go diff --git a/checker/check_request.go b/checker/check_request.go index a9674283e25..661d3d8c7ba 100644 --- a/checker/check_request.go +++ b/checker/check_request.go @@ -33,6 +33,6 @@ type CheckRequest struct { // Note: Ultimately Log will be removed and replaced by // CLogger. Logf func(s string, f ...interface{}) - CLogger CheckLogger + Dlogger DetailLogger Owner, Repo string } diff --git a/checker/check_result.go b/checker/check_result.go index a9a41642f44..5c4f7ad5477 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -16,6 +16,7 @@ package checker import ( "errors" + "fmt" scorecarderrors "github.com/ossf/scorecard/errors" ) @@ -26,185 +27,184 @@ const ( MinResultConfidence = 0 ) +// UPGRADEv2: to remove. // ErrorDemoninatorZero indicates the denominator for a proportional result is 0. var ErrorDemoninatorZero = errors.New("internal error: denominator is 0") -// Types of details. -type DetailType int - -const ( - DetailFail DetailType = iota - DetailPass - DetailInfo - DetailWarn - DetailDebug -) - -// CheckDetail contains information for each detail. -//nolint:govet -type CheckDetail struct { - Type DetailType // Any of DetailFail, DetailPass, DetailInfo. - Code string // A string identifying the sub-check, e.g. to lookup remediation info. - Desc string // A short string representation of the information. -} - -// Types of results. +//nolint const ( - ResultPass = 0 - ResultFail = 1 - ResultDontKnow = 2 + MaxResultScore = 10 + HalfResultScore = 5 + MinResultScore = 0 + InconclusiveResultScore = -1 ) +//nolint type CheckResult struct { - Error error `json:"-"` - Name string - Details []string - Details2 []CheckDetail `json:"-"` - Confidence int - // Note: Pass2 will ultimately be renamed - // as Pass. + // Old structure + Error error `json:"-"` + Name string + Details []string + Confidence int Pass bool - Pass2 int ShouldRetry bool `json:"-"` -} -// Will be removed. -func MakeInconclusiveResult(name string, err error) CheckResult { - return CheckResult{ - Name: name, - Pass: false, - Confidence: 0, - Pass2: ResultDontKnow, - Error: scorecarderrors.MakeLowConfidenceError(err), - } + // UPGRADEv2: New structure. Omitting unchanged Name field + // for simplicity. + Version int // 2. Default value of 0 indicates old structure + Error2 error `json:"-"` // Runtime error indicate a filure to run the check. + Details2 []CheckDetail `json:"-"` // Details of tests and sub-checks + Score2 int `json:"-"` // {[0...1], -1 = Inconclusive} + Reason2 string `json:"-"` // A sentence describing the check result (score, etc) } -// TODO: these functions should set the details as well. -func MakeInternalErrorResult(name string, err error) CheckResult { - return CheckResult{ - Name: name, - Pass: false, - Confidence: 0, - Pass2: ResultDontKnow, - Error: scorecarderrors.MakeLowConfidenceError(err), +// CreateResultWithScore is used when +// the check runs without runtime errors and want to assign a +// specific score. +func CreateResultWithScore(name, reason string, score int) CheckResult { + pass := true + //nolint + if score < 8 { + pass = false } -} - -func MakeInconclusiveResult2(name string, c *CheckRequest, reason string) CheckResult { - c.CLogger.Warn("lowering result confidence to %d because %s", 0, reason) return CheckResult{ - Name: name, - Pass: false, - Confidence: 0, - Pass2: ResultDontKnow, - Error: nil, + Name: name, + // Old structure. + Error: nil, + Confidence: MaxResultScore, + Pass: pass, + ShouldRetry: false, + // New structure. + //nolint + Version: 2, + Error2: nil, + Score2: score, + Reason2: reason, } } -func MakePassResult(name string) CheckResult { +// CreateProportionalScoreResult is used when +// the check runs without runtime errors and we assign a +// proportional score. This may be used if a check contains +// multiple tests and we want to assign a score proportional +// the the number of tests that succeeded. +func CreateProportionalScoreResult(name, reason string, b, t int) CheckResult { + pass := true + //nolint + score := 10 * b / t + //nolint + if score < 8 { + pass = false + } return CheckResult{ - Name: name, - Pass: true, - Pass2: ResultPass, - Confidence: MaxResultConfidence, - Error: nil, + Name: name, + // Old structure. + Error: nil, + //nolint + Confidence: 10, + Pass: pass, + ShouldRetry: false, + // New structure. + //nolint + Version: 2, + Error2: nil, + Score2: 10 * b / t, + Reason2: fmt.Sprintf("%v -- code normalized to %d", reason, score), } } -func MakePassResultWithHighConfidence(name string) CheckResult { - return CheckResult{ - Name: name, - Pass: true, - Pass2: ResultPass, - Confidence: MaxResultConfidence, - Error: nil, - } +// CreateMaxScoreResult is used when +// the check runs without runtime errors and we can assign a +// maximum score to the result. +func CreateMaxScoreResult(name, reason string) CheckResult { + return CreateResultWithScore(name, reason, MaxResultScore) } -func MakePassResultWithHighConfidenceAndReason(name string, c *CheckRequest, reason string) CheckResult { - c.CLogger.Pass("%s", reason) - return CheckResult{ - Name: name, - Pass: true, - Pass2: ResultPass, - Confidence: MaxResultConfidence, - Error: nil, - } +// CreateMinScoreResult is used when +// the check runs without runtime errors and we can assign a +// minimum score to the result. +func CreateMinScoreResult(name, reason string) CheckResult { + return CreateResultWithScore(name, reason, MinResultScore) } -func MakePassResultWithHighConfidenceAndReasonAndCode(name string, c *CheckRequest, code, reason string) CheckResult { - c.CLogger.PassWithCode(code, reason) +// CreateInconclusiveResult is used when +// the check runs without runtime errors, but we don't +// have enough evidence to set a score. +func CreateInconclusiveResult(name, reason string) CheckResult { return CheckResult{ - Name: name, - Pass: true, - Pass2: ResultPass, - Confidence: MaxResultConfidence, - Error: nil, + Name: name, + // Old structure. + Confidence: 0, + Pass: false, + ShouldRetry: false, + // New structure. + //nolint + Version: 2, + Score2: InconclusiveResultScore, + Reason2: reason, } } -func MakePassResultWithLowConfidenceAndReason(name string, c *CheckRequest, conf int, reason string) CheckResult { - c.CLogger.Warn("%s (lowering confidence to %d)", reason, conf) +// CreateRuntimeErrorResult is used when the check fails to run because of a runtime error. +func CreateRuntimeErrorResult(name string, e error) CheckResult { return CheckResult{ - Name: name, - Pass: true, - Pass2: ResultPass, - Confidence: conf, - Error: nil, + Name: name, + // Old structure. + Error: e, + Confidence: 0, + Pass: false, + ShouldRetry: false, + // New structure. + //nolint + Version: 2, + Error2: e, + Score2: InconclusiveResultScore, + Reason2: e.Error(), // Note: message already accessible by caller thru `Error`. } } -// Will be removed. -func MakeFailResult(name string, err error) CheckResult { - return CheckResult{ - Name: name, - Pass: false, - Pass2: ResultFail, - Confidence: MaxResultConfidence, - Error: err, +// UPGRADEv2: will be renamed. +func MakeAndResult2(checks ...CheckResult) CheckResult { + if len(checks) == 0 { + // That should never happen. + panic("MakeResult called with no checks") } -} -func MakeFailResultWithHighConfidence(name string) CheckResult { - return CheckResult{ - Name: name, - Pass: false, - Pass2: ResultFail, - Confidence: MaxResultConfidence, - Error: nil, + worseResult := checks[0] + + for _, result := range checks[1:] { + if result.Score2 < worseResult.Score2 { + worseResult = result + } } + return worseResult } -func MakeFailResultWithHighConfidenceAndReason(name string, c *CheckRequest, reason string) CheckResult { - c.CLogger.Info("%s", reason) +// UPGRADEv2: will be removed. +func MakeInconclusiveResult(name string, err error) CheckResult { return CheckResult{ Name: name, Pass: false, - Pass2: ResultFail, - Confidence: MaxResultConfidence, - Error: nil, + Confidence: 0, + Error: scorecarderrors.MakeLowConfidenceError(err), } } -func MakeFailResultWithHighConfidenceAndReasonAndCode(name string, c *CheckRequest, code, reason string) CheckResult { - c.CLogger.FailWithCode(code, reason) +func MakePassResult(name string) CheckResult { return CheckResult{ Name: name, - Pass: false, - Pass2: ResultFail, + Pass: true, Confidence: MaxResultConfidence, Error: nil, } } -func MakeFailResultLowConfidenceAndReason(name string, c *CheckRequest, conf int, reason string) CheckResult { - c.CLogger.Fail("%s (lowering confidence to %d)", reason, conf) +func MakeFailResult(name string, err error) CheckResult { return CheckResult{ Name: name, Pass: false, - Pass2: ResultFail, - Confidence: conf, - Error: nil, + Confidence: MaxResultConfidence, + Error: err, } } @@ -212,7 +212,6 @@ func MakeRetryResult(name string, err error) CheckResult { return CheckResult{ Name: name, Pass: false, - Pass2: ResultDontKnow, ShouldRetry: true, Error: scorecarderrors.MakeRetryError(err), } @@ -229,7 +228,6 @@ func MakeProportionalResult(name string, numerator int, denominator int, return CheckResult{ Name: name, Pass: false, - Pass2: ResultFail, Confidence: MaxResultConfidence, } } @@ -238,7 +236,6 @@ func MakeProportionalResult(name string, numerator int, denominator int, return CheckResult{ Name: name, Pass: true, - Pass2: ResultPass, Confidence: int(actual * MaxResultConfidence), } } @@ -246,7 +243,6 @@ func MakeProportionalResult(name string, numerator int, denominator int, return CheckResult{ Name: name, Pass: false, - Pass2: ResultFail, Confidence: MaxResultConfidence - int(actual*MaxResultConfidence), } } @@ -269,7 +265,6 @@ func isMinResult(result, min CheckResult) bool { func MakeAndResult(checks ...CheckResult) CheckResult { minResult := CheckResult{ Pass: true, - Pass2: ResultPass, Confidence: MaxResultConfidence, } diff --git a/checker/check_runner.go b/checker/check_runner.go index 06a7d705f98..30a530bfba5 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -39,67 +39,49 @@ type CheckFn func(*CheckRequest) CheckResult type CheckNameToFnMap map[string]CheckFn -type logger struct { - // Note: messages2 will ultimately - // be renamed to messages. - messages []string - messages2 []CheckDetail -} - -type CheckLogger struct { - l *logger -} - -func (l *CheckLogger) FailWithCode(code, desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailFail, Code: code, Desc: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) -} - -func (l *CheckLogger) Fail(desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailFail, Code: "", Desc: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) -} - -func (l *CheckLogger) Pass(desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailPass, Code: "", Desc: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) -} +// Types of details. +type DetailType int -func (l *CheckLogger) PassWithCode(code, desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailPass, Code: code, Desc: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) -} +const ( + DetailInfo DetailType = iota + DetailWarn + DetailDebug +) -func (l *CheckLogger) Info(desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailInfo, Code: "", Desc: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) +// CheckDetail contains information for each detail. +//nolint:govet +type CheckDetail struct { + Type DetailType // Any of DetailWarn, DetailInfo, DetailDebug. + Msg string // A short string explaining why the details was recorded/logged.. } -func (l *CheckLogger) InfoWithCode(code, desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailInfo, Code: code, Desc: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) +// UPGRADEv2: messages2 will ultimately +// be renamed to messages. +type logger struct { + messages []string + messages2 []CheckDetail } -func (l *CheckLogger) Warn(desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailWarn, Code: "", Desc: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) +type DetailLogger struct { + l *logger } -func (l *CheckLogger) WarnWithCode(code, desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailWarn, Code: code, Desc: fmt.Sprintf(desc, args...)} +func (l *DetailLogger) Info(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailInfo, Msg: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } -func (l *CheckLogger) Debug(desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailDebug, Code: "", Desc: fmt.Sprintf(desc, args...)} +func (l *DetailLogger) Warn(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailWarn, Msg: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } -func (l *CheckLogger) DebugWithCode(code, desc string, args ...interface{}) { - cd := CheckDetail{Type: DetailDebug, Code: code, Desc: fmt.Sprintf(desc, args...)} +func (l *DetailLogger) Debug(desc string, args ...interface{}) { + cd := CheckDetail{Type: DetailDebug, Msg: fmt.Sprintf(desc, args...)} l.l.messages2 = append(l.l.messages2, cd) } +// UPGRADEv2: to remove. func (l *logger) Logf(s string, f ...interface{}) { l.messages = append(l.messages, fmt.Sprintf(s, f...)) } @@ -127,21 +109,24 @@ func (r *Runner) Run(ctx context.Context, f CheckFn) CheckResult { var res CheckResult var l logger - var cl CheckLogger + var dl DetailLogger for retriesRemaining := checkRetries; retriesRemaining > 0; retriesRemaining-- { checkRequest := r.CheckRequest checkRequest.Ctx = ctx l = logger{} - cl = CheckLogger{l: &l} + dl = DetailLogger{l: &l} + // UPGRADEv2: to remove. checkRequest.Logf = l.Logf - checkRequest.CLogger = cl + checkRequest.Dlogger = dl res = f(&checkRequest) + // UPGRADEv2: to fix using proper error check. if res.ShouldRetry && !strings.Contains(res.Error.Error(), "invalid header field value") { checkRequest.Logf("error, retrying: %s", res.Error) continue } break } + // UPGRADEv2: to remove. res.Details = l.messages res.Details2 = l.messages2 @@ -158,6 +143,34 @@ func Bool2int(b bool) int { return 0 } +// UPGRADEv2: will be renamed. +func MultiCheckOr2(fns ...CheckFn) CheckFn { + return func(c *CheckRequest) CheckResult { + var maxResult CheckResult //{Version:2} + + for _, fn := range fns { + result := fn(c) + + if result.Score2 > maxResult.Score2 { + maxResult = result + } + } + return maxResult + } +} + +func MultiCheckAnd2(fns ...CheckFn) CheckFn { + return func(c *CheckRequest) CheckResult { + var checks []CheckResult + for _, fn := range fns { + res := fn(c) + checks = append(checks, res) + } + return MakeAndResult2(checks...) + } +} + +// UPGRADEv2: will be removed. // MultiCheckOr returns the best check result out of several ones performed. func MultiCheckOr(fns ...CheckFn) CheckFn { return func(c *CheckRequest) CheckResult { diff --git a/checks/automatic_dependency_update.go b/checks/automatic_dependency_update.go index 79e75189873..6957b8ff53c 100644 --- a/checks/automatic_dependency_update.go +++ b/checks/automatic_dependency_update.go @@ -31,28 +31,26 @@ func init() { func AutomaticDependencyUpdate(c *checker.CheckRequest) checker.CheckResult { r, err := CheckIfFileExists2(checkAutomaticDependencyUpdate, c, fileExists) if err != nil { - return checker.MakeInternalErrorResult(checkAutomaticDependencyUpdate, err) + return checker.CreateRuntimeErrorResult(checkAutomaticDependencyUpdate, err) } if !r { - return checker.MakeInconclusiveResult2(checkAutomaticDependencyUpdate, c, "no configuration file found in the repo") + return checker.CreateMinScoreResult(checkAutomaticDependencyUpdate, "no tool detected [dependabot|renovabot]") } - // High confidence result. - // We need not give a reason since it's explained by the calls to - // `cl.Pass` in fileExists. - return checker.MakePassResultWithHighConfidence(checkAutomaticDependencyUpdate) + // High score result. + return checker.CreateMaxScoreResult(checkAutomaticDependencyUpdate, "tool detected") } // fileExists will validate the if frozen dependencies file name exists. -func fileExists(name string, cl checker.CheckLogger) (bool, error) { +func fileExists(name string, dl checker.DetailLogger) (bool, error) { switch strings.ToLower(name) { case ".github/dependabot.yml": - cl.Pass("dependabot config found: %s", name) + dl.Info("dependabot detected : %s", name) return true, nil // https://docs.renovatebot.com/configuration-options/ case ".github/renovate.json", ".github/renovate.json5", ".renovaterc.json", "renovate.json", "renovate.json5", ".renovaterc": - cl.Pass("renovate config found: %s", name) + dl.Info("renovate detected: %s", name) return true, nil default: return false, nil diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 6c3d4bfc689..56e4ddfa357 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -22,6 +22,7 @@ import ( "github.com/h2non/filetype/types" "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" ) //nolint @@ -36,23 +37,17 @@ const checkBinaryArtifacts string = "Binary-Artifacts" func binaryArtifacts(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*", false, c, checkBinaryFileContent) if err != nil { - // TODO: check for the repo retry error, which should be a common - // scorecard error independent of the underlying implementation. - return checker.MakeInternalErrorResult(checkBinaryArtifacts, err) + return checker.CreateRuntimeErrorResult(checkBinaryArtifacts, err) } if !r { - // We need not provid a reason because it's already done - // in checkBinaryFileContent via `Pass` call. - return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + return checker.CreateMinScoreResult(checkBinaryArtifacts, "binaries present in source code") } - // High confidence result. - // We provide a reason to help the user. - return checker.MakePassResultWithHighConfidenceAndReason(checkBinaryArtifacts, c, "no binary files found in the repo") + return checker.CreateMaxScoreResult(checkBinaryArtifacts, "no binaries found in the repo") } func checkBinaryFileContent(path string, content []byte, - cl checker.CheckLogger) (bool, error) { + dl checker.DetailLogger) (bool, error) { binaryFileTypes := map[string]bool{ "crx": true, "deb": true, @@ -93,15 +88,15 @@ func checkBinaryFileContent(path string, content []byte, var t types.Type var err error if t, err = filetype.Get(content); err != nil { - return false, fmt.Errorf("failed in getting the content type %w", err) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprint("filetype.Get:%v", err.Error())) } if _, ok := binaryFileTypes[t.Extension]; ok { - cl.Fail("binary-artifact found: %s", path) + dl.Warn("binary found: %s", path) return false, nil } else if _, ok := binaryFileTypes[filepath.Ext(path)]; ok { // Falling back to file based extension. - cl.Fail("binary-artifact found: %s", path) + dl.Warn("binary found: %s", path) return false, nil } diff --git a/checks/checkforcontent.go b/checks/checkforcontent.go index a8967a407c1..03a13b80cdc 100644 --- a/checks/checkforcontent.go +++ b/checks/checkforcontent.go @@ -15,17 +15,14 @@ package checks import ( - "errors" "fmt" "path" "strings" "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" ) -// ErrReadFile indicates the header size does not match the size of the file. -var ErrReadFile = errors.New("could not read entire file") - // IsMatchingPath uses 'pattern' to shell-match the 'path' and its filename // 'caseSensitive' indicates the match should be case-sensitive. Default: no. func isMatchingPath(pattern, fullpath string, caseSensitive bool) (bool, error) { @@ -37,13 +34,13 @@ func isMatchingPath(pattern, fullpath string, caseSensitive bool) (bool, error) filename := path.Base(fullpath) match, err := path.Match(pattern, fullpath) if err != nil { - return false, fmt.Errorf("match error: %w", err) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalFilenameMatch, err)) } // No match on the fullpath, let's try on the filename only. if !match { if match, err = path.Match(pattern, filename); err != nil { - return false, fmt.Errorf("match error: %w", err) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalFilenameMatch, err)) } } @@ -77,7 +74,6 @@ func CheckFilesContent(checkName, shellPathFnPattern string, // Filter out files based on path/names using the pattern. b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive) if err != nil { - c.Logf("error during isMatchingPath: %v", err) return false } return b @@ -106,11 +102,12 @@ func CheckFilesContent(checkName, shellPathFnPattern string, return checker.MakeFailResult(checkName, nil) } +// UPGRADEv2: to rename to CheckFilesContent. func CheckFilesContent2(shellPathFnPattern string, caseSensitive bool, c *checker.CheckRequest, onFileContent func(path string, content []byte, - cl checker.CheckLogger) (bool, error), + dl checker.DetailLogger) (bool, error), ) (bool, error) { predicate := func(filepath string) bool { // Filter out Scorecard's own test files. @@ -120,7 +117,6 @@ func CheckFilesContent2(shellPathFnPattern string, // Filter out files based on path/names using the pattern. b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive) if err != nil { - c.CLogger.Warn("%v", err) return false } return b @@ -132,7 +128,7 @@ func CheckFilesContent2(shellPathFnPattern string, return false, err } - rr, err := onFileContent(file, content, c.CLogger) + rr, err := onFileContent(file, content, c.Dlogger) if err != nil { return false, err } diff --git a/checks/checkforfile.go b/checks/checkforfile.go index d5f031f1e71..8c9d96851f0 100644 --- a/checks/checkforfile.go +++ b/checks/checkforfile.go @@ -46,9 +46,9 @@ func CheckIfFileExists(checkName string, c *checker.CheckRequest, onFile func(na } func CheckIfFileExists2(checkName string, c *checker.CheckRequest, onFile func(name string, - cl checker.CheckLogger) (bool, error)) (bool, error) { + dl checker.DetailLogger) (bool, error)) (bool, error) { for _, filename := range c.RepoClient.ListFiles(func(string) bool { return true }) { - rr, err := onFile(filename, c.CLogger) + rr, err := onFile(filename, c.Dlogger) if err != nil { return false, err } diff --git a/checks/code_review.go b/checks/code_review.go index 419c423c60e..81c8f1df4a2 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -89,11 +89,9 @@ func DoesCodeReview(c *checker.CheckRequest) checker.CheckResult { "labelsToAnalyze": githubv4.Int(labelsToAnalyze), } if err := c.GraphClient.Query(c.Ctx, &prHistory, vars); err != nil { - // Note: this error should not be wrapped. We should - // return a scorecard error instead. - return checker.MakeInternalErrorResult(checkCodeReview, err) + return checker.CreateRuntimeErrorResult(checkCodeReview, err) } - return checker.MultiCheckOr( + return checker.MultiCheckOr2( isPrReviewRequired, githubCodeReview, prowCodeReview, @@ -115,7 +113,7 @@ func githubCodeReview(c *checker.CheckRequest) checker.CheckResult { foundApprovedReview := false for _, r := range pr.LatestReviews.Nodes { if r.State == "APPROVED" { - c.CLogger.Debug("found review approved pr: %d", pr.Number) + c.Dlogger.Debug("found review approved pr: %d", pr.Number) totalReviewed++ foundApprovedReview = true break @@ -127,7 +125,7 @@ func githubCodeReview(c *checker.CheckRequest) checker.CheckResult { // time on clicking the approve button. if !foundApprovedReview { if !pr.MergeCommit.AuthoredByCommitter { - c.CLogger.Debug("found pr with committer different than author: %d", pr.Number) + c.Dlogger.Debug("found pr with committer different than author: %d", pr.Number) totalReviewed++ } } @@ -142,9 +140,9 @@ func isPrReviewRequired(c *checker.CheckRequest) checker.CheckResult { if prHistory.Repository.DefaultBranchRef.BranchProtectionRule.RequiredApprovingReviewCount >= 1 { // If the default value is 0 when we cannot retrieve the value, // a non-zero value means we're confident it's enabled. - return checker.MakePassResultWithHighConfidenceAndReason(checkCodeReview, c, "branch protection for default branch is enabled") + return checker.CreateMaxScoreResult(checkCodeReview, "branch protection for default branch is enabled") } - return checker.MakeInconclusiveResult2(checkCodeReview, c, "cannot determine if branch protection is enabled") + return checker.CreateInconclusiveResult(checkCodeReview, "cannot determine if branch protection is enabled") } func prowCodeReview(c *checker.CheckRequest) checker.CheckResult { @@ -185,7 +183,7 @@ func commitMessageHints(c *checker.CheckRequest) checker.CheckResult { } } if isBot { - c.CLogger.Debug("skip commit from bot account: %s", committer) + c.Dlogger.Debug("skip commit from bot account: %s", committer) continue } @@ -200,27 +198,15 @@ func commitMessageHints(c *checker.CheckRequest) checker.CheckResult { } } - if totalReviewed > 0 { - reason := fmt.Sprintf("Gerrit code reviews found for %v commits out of the last %v", totalReviewed, total) - if total == totalReviewed { - return checker.MakePassResultWithHighConfidenceAndReason(checkCodeReview, c, reason) - } else { - return checker.MakeFailResultLowConfidenceAndReason(checkCodeReview, c, checker.HalfResultConfidence, reason) - } - } - return createResult(c, "Gerrit", totalReviewed, total) } func createResult(c *checker.CheckRequest, reviewName string, reviewed, total int) checker.CheckResult { - if reviewed > 0 { + if total > 0 { reason := fmt.Sprintf("%s code reviews found for %v commits out of the last %v", reviewName, reviewed, total) - if total == reviewed { - return checker.MakePassResultWithHighConfidenceAndReason(checkCodeReview, c, reason) - } else { - return checker.MakeFailResultLowConfidenceAndReason(checkCodeReview, c, checker.HalfResultConfidence, reason) - } + return checker.CreateProportionalScoreResult(checkCodeReview, reason, reviewed, total) + } - return checker.MakeInconclusiveResult2(checkCodeReview, c, fmt.Sprintf("no %s reviews found", reviewName)) + return checker.CreateInconclusiveResult(checkCodeReview, fmt.Sprintf("no %s commits found", reviewName)) } diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index d0c0bd3548b..2d86b8386dd 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -15,26 +15,19 @@ package checks import ( - "errors" "fmt" "regexp" "strings" "github.com/moby/buildkit/frontend/dockerfile/parser" - "gopkg.in/yaml.v2" - "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" + "gopkg.in/yaml.v2" ) // checkFrozenDeps is the registered name for FrozenDeps. const checkFrozenDeps = "Frozen-Deps" -// ErrInvalidDockerfile : Invalid docker file. -var ErrInvalidDockerfile = errors.New("invalid docker file") - -// ErrEmptyFile : Invalid docker file. -var ErrEmptyFile = errors.New("file has no content") - // Structure for workflow config. // We only declare the fields we need. // Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions @@ -80,52 +73,42 @@ func FrozenDeps(c *checker.CheckRequest) checker.CheckResult { func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*", false, c, validateShellScriptDownloads) if err != nil { - // TODO: check for the repo retry error, which should be a common - // scorecard error independent of the underlying implementation. - return checker.MakeInternalErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - // We need not provide a reason/code because it's already done - // in validateDockerfile via `Fail` call. - return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + return checker.CreateMinScoreResult(checkFrozenDeps, "insecure (unpinned) dependency donwloads found in shell scripts") } - return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, - "BinaryDownload", "no binary downloads found in shell scripts") + return checker.CreateMaxScoreResult(checkFrozenDeps, "no insecure (unpinned) dependency downloads found in shell scripts") } func validateShellScriptDownloads(pathfn string, content []byte, - cl checker.CheckLogger) (bool, error) { + dl checker.DetailLogger) (bool, error) { // Validate the file type. if !isShellScriptFile(pathfn, content) { return true, nil } - return validateShellFile(pathfn, content, cl) + return validateShellFile(pathfn, content, dl) } func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfileDownloads) if err != nil { - // TODO: check for the repo retry error, which should be a common - // scorecard error independent of the underlying implementation. - return checker.MakeInternalErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - // We need not provide a reason/code because it's already done - // in validateDockerfile via `Fail` call. - return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + return checker.CreateMinScoreResult(checkFrozenDeps, "insecure (unpinned) dependency donwloads found in Dockerfiles") } - return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, - "BinaryDownload", "no binary downloads found in Dockerfiles") + return checker.CreateMaxScoreResult(checkFrozenDeps, "no insecure (unpinned) dependency downloads found in Dockerfiles") } func validateDockerfileDownloads(pathfn string, content []byte, - cl checker.CheckLogger) (bool, error) { + dl checker.DetailLogger) (bool, error) { contentReader := strings.NewReader(string(content)) res, err := parser.Parse(contentReader) if err != nil { - return false, fmt.Errorf("cannot read dockerfile content: %w", err) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidDockerFile, err)) } // nolint: prealloc @@ -145,7 +128,7 @@ func validateDockerfileDownloads(pathfn string, content []byte, } if len(valueList) == 0 { - return false, ErrParsingDockerfile + return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalInvalidDockerFile.Error()) } // Build a file content. @@ -153,28 +136,23 @@ func validateDockerfileDownloads(pathfn string, content []byte, bytes = append(bytes, cmd...) bytes = append(bytes, '\n') } - return validateShellFile(pathfn, bytes, cl) + return validateShellFile(pathfn, bytes, dl) } func isDockerfilePinned(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfile) if err != nil { - // TODO: check for the repo retry error, which should be a common - // scorecard error independent of the underlying implementation. - return checker.MakeInternalErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - // We need not provide a reason/code because it's already done - // in validateDockerfile via `Fail` call. - return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + return checker.CreateMinScoreResult(checkFrozenDeps, "unpinned dependencies found Dockerfiles") } - return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, - "Dockerfile", "Dockerfile dependencies are pinned") + return checker.CreateMaxScoreResult(checkFrozenDeps, "Dockerfile dependencies are pinned") } func validateDockerfile(pathfn string, content []byte, - cl checker.CheckLogger) (bool, error) { + dl checker.DetailLogger) (bool, error) { // Users may use various names, e.g., // Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template // Templates may trigger false positives, e.g. FROM { NAME }. @@ -189,7 +167,7 @@ func validateDockerfile(pathfn string, content []byte, pinnedAsNames := make(map[string]bool) res, err := parser.Parse(contentReader) if err != nil { - return false, fmt.Errorf("cannot read dockerfile content: %w", err) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidDockerFile, err)) } for _, child := range res.AST.Children { @@ -227,25 +205,25 @@ func validateDockerfile(pathfn string, content []byte, // Not pinned. ret = false - cl.FailWithCode("Dockerfile", "%v has non-pinned dependency '%v'", pathfn, name) + dl.Warn("unpinned dependency detected in %v: '%v'", pathfn, name) // FROM name. case len(valueList) == 1: name := valueList[0] if !regex.Match([]byte(name)) { ret = false - cl.FailWithCode("Dockerfile", "%v has non-pinned dependency '%v'", pathfn, name) + dl.Warn("unpinned dependency detected in %v: '%v'", pathfn, name) } default: // That should not happen. - return false, ErrInvalidDockerfile + return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalInvalidDockerFile.Error()) } } // The file should have at least one FROM statement. if !fromFound { - return false, ErrInvalidDockerfile + return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalInvalidDockerFile.Error()) } return ret, nil @@ -254,30 +232,26 @@ func validateDockerfile(pathfn string, content []byte, func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2(".github/workflows/*", false, c, validateGitHubWorkflowShellScriptDownloads) if err != nil { - // TODO: check for the repo retry error, which should be a common - // scorecard error independent of the underlying implementation. - return checker.MakeInternalErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - // We need not provide a reason/code because it's already done - // in validateGitHubWorkflowShellScriptDownloads via `Fail` call. - return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + return checker.CreateMinScoreResult(checkFrozenDeps, "insecure (unpinned) dependency donwloads found in GitHub workflows") } - return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, - "BinaryDownload", "no binary download found in GitHub workflows") + return checker.CreateMaxScoreResult(checkFrozenDeps, "no insecure (unpinned) dependency downloads found in GitHub workflows") } func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, - cl checker.CheckLogger) (bool, error) { + dl checker.DetailLogger) (bool, error) { if len(content) == 0 { - return false, ErrEmptyFile + return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalEmptyFile.Error()) } var workflow gitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { - return false, fmt.Errorf("cannot unmarshal file %v\n%v: %w", pathfn, string(content), err) + return false, sce.Create(sce.ErrRunFailure, + fmt.Sprintf("%v:%v:%v:%v", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) } githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`) @@ -314,7 +288,7 @@ func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, } if scriptContent != "" { - validated, err = validateShellFile(pathfn, []byte(scriptContent), cl) + validated, err = validateShellFile(pathfn, []byte(scriptContent), dl) if err != nil { return false, err } @@ -327,32 +301,26 @@ func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2(".github/workflows/*", true, c, validateGitHubActionWorkflow) if err != nil { - // TODO: check for the repo retry error, which should be a common - // scorecard error independent of the underlying implementation. - return checker.MakeInternalErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - // We need not provide a reason/code because it's already done - // in validateGitHubActionWorkflow via `Fail` call. - return checker.MakeFailResultWithHighConfidence(checkBinaryArtifacts) + return checker.CreateMinScoreResult(checkFrozenDeps, "GitHub actions are not pinned") } - // High confidence result. - // We provide a reason to help the user. - return checker.MakePassResultWithHighConfidenceAndReasonAndCode(checkBinaryArtifacts, c, - "GitHubActions", "GitHub actions' dependencies are pinned") + return checker.CreateMinScoreResult(checkFrozenDeps, "GitHub actions are pinned") } // Check file content. -func validateGitHubActionWorkflow(pathfn string, content []byte, cl checker.CheckLogger) (bool, error) { +func validateGitHubActionWorkflow(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { if len(content) == 0 { - return false, ErrEmptyFile + return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalEmptyFile.Error()) } var workflow gitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { - return false, fmt.Errorf("cannot unmarshal file %v\n%v: %w", pathfn, string(content), err) + return false, sce.Create(sce.ErrRunFailure, + fmt.Sprintf("%v:%v:%v:%v", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) } hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`) @@ -368,7 +336,7 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, cl checker.Chec match := hashRegex.Match([]byte(step.Uses)) if !match { ret = false - cl.FailWithCode("GitHubActions", "%v has non-pinned dependency '%v' (job '%v')", pathfn, step.Uses, jobName) + dl.Warn("unpinned dependency detected in %v: '%v' (job '%v')", pathfn, step.Uses, jobName) } } } @@ -381,48 +349,47 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, cl checker.Chec func isPackageManagerLockFilePresent(c *checker.CheckRequest) checker.CheckResult { r, err := CheckIfFileExists2(checkFrozenDeps, c, validatePackageManagerFile) if err != nil { - return checker.MakeInternalErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - return checker.MakeFailResultWithHighConfidenceAndReasonAndCode(checkFrozenDeps, c, - "LockFile", "no lock file found in the repo") + return checker.CreateInconclusiveResult(checkFrozenDeps, "no lock files detected for a package manager") } - // High confidence result. - // We don't pass a `reason` because it's already done - // thru calls to `Pass` in validatePackageManagerFile. - return checker.MakePassResultWithHighConfidence(checkFrozenDeps) + return checker.CreateMaxScoreResult(checkFrozenDeps, "lock file detected for a package manager") } // validatePackageManagerFile will validate the if frozen dependecies file name exists. // TODO(laurent): need to differentiate between libraries and programs. // TODO(laurent): handle multi-language repos. -func validatePackageManagerFile(name string, cl checker.CheckLogger) (bool, error) { +func validatePackageManagerFile(name string, dl checker.DetailLogger) (bool, error) { switch strings.ToLower(name) { - case "go.mod", "go.sum": - cl.PassWithCode("LockFile", "go modules found: %s", name) + // TODO(laurent): "go.mod" is for libraries + case "go.sum": + dl.Info("go lock file detected: %s", name) return true, nil case "vendor/", "third_party/", "third-party/": - cl.PassWithCode("LockFile", "vendor dir found: %s", name) + dl.Info("vendoring detected in: %s", name) return true, nil case "package-lock.json", "npm-shrinkwrap.json": - cl.PassWithCode("LockFile", "nodejs packages found: %s", name) + dl.Info("javascript lock file detected: %s", name) return true, nil // TODO(laurent): add check for hashbased pinning in requirements.txt - https://davidwalsh.name/hashin - case "requirements.txt", "pipfile.lock": - cl.PassWithCode("LockFile", "python requirements found: %s", name) + // Note: because requirements.txt does not handle transitive dependencies, we consider it + // not a lock file, until we have remediation steps for pip-build. + case "pipfile.lock": + dl.Info("python lock file detected: %s", name) return true, nil case "gemfile.lock": - cl.PassWithCode("LockFile", "ruby gems found: %s", name) + dl.Info("ruby lock file detected: %s", name) return true, nil case "cargo.lock": - cl.PassWithCode("LockFile", "rust crates found: %s", name) + dl.Info("rust lock file detected: %s", name) return true, nil case "yarn.lock": - cl.PassWithCode("LockFile", "yarn packages found: %s", name) + dl.Info("yarn lock file detected: %s", name) return true, nil case "composer.lock": - cl.PassWithCode("LockFile", "composer packages found: %s", name) + dl.Info("composer lock file detected: %s", name) return true, nil default: return false, nil diff --git a/checks/permissions.go b/checks/permissions.go index 838dfd0cebb..8a538f82320 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -22,6 +22,7 @@ import ( "gopkg.in/yaml.v2" "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" ) const CheckPermissions = "Token-Permissions" @@ -121,7 +122,7 @@ func validateReadPermissions(config map[interface{}]interface{}, path string, func validateGitHubActionTokenPermissions(path string, content []byte, logf func(s string, f ...interface{})) (bool, error) { if len(content) == 0 { - return false, ErrEmptyFile + return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalEmptyFile.Error()) } var workflow map[interface{}]interface{} diff --git a/checks/shell_download_validate.go b/checks/shell_download_validate.go index ec549aa41ce..cb9138e7d9e 100644 --- a/checks/shell_download_validate.go +++ b/checks/shell_download_validate.go @@ -17,7 +17,6 @@ package checks import ( "bufio" "bytes" - "errors" "fmt" "net/url" "path" @@ -26,20 +25,10 @@ import ( "strings" "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" "mvdan.cc/sh/v3/syntax" ) -// ErrParsingDockerfile indicates a problem parsing the dockerfile. -var ErrParsingDockerfile = errors.New("file cannot be parsed") - -// ErrParsingShellCommand indicates a problem parsing a shell command. -var ErrParsingShellCommand = errors.New("shell command cannot be parsed") - -const ( - binaryDownload = "BinaryDownload" - packageInstall = "PackageInstall" -) - // List of interpreters. var pythonInterpreters = []string{"python", "python3", "python2.7"} @@ -112,7 +101,7 @@ func getWgetOutputFile(cmd []string) (pathfn string, ok bool, err error) { u, err := url.Parse(cmd[i]) if err != nil { - return "", false, fmt.Errorf("url.Parse: %w", err) + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) } return path.Base(u.Path), true, nil } @@ -131,7 +120,7 @@ func getGsutilOutputFile(cmd []string) (pathfn string, ok bool, err error) { // Directory. u, err := url.Parse(cmd[i]) if err != nil { - return "", false, fmt.Errorf("url.Parse: %w", err) + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) } return filepath.Join(filepath.Dir(pathfn), path.Base(u.Path)), true, nil } @@ -156,7 +145,7 @@ func getAWSOutputFile(cmd []string) (pathfn string, ok bool, err error) { if filepath.Clean(filepath.Dir(ofile)) == filepath.Clean(ofile) { u, err := url.Parse(ifile) if err != nil { - return "", false, fmt.Errorf("url.Parse: %w", err) + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) } return filepath.Join(filepath.Dir(ofile), path.Base(u.Path)), true, nil } @@ -272,7 +261,7 @@ func extractCommand(cmd interface{}) ([]string, bool) { } func isFetchPipeExecute(node syntax.Node, cmd, pathfn string, - cl checker.CheckLogger) bool { + dl checker.DetailLogger) bool { // BinaryCmd {Op=|, X=CallExpr{Args={curl, -s, url}}, Y=CallExpr{Args={bash,}}}. bc, ok := node.(*syntax.BinaryCmd) if !ok { @@ -301,8 +290,7 @@ func isFetchPipeExecute(node syntax.Node, cmd, pathfn string, return false } - cl.FailWithCode(binaryDownload, "%v is fetching and executing non-pinned program '%v'", - pathfn, cmd) + dl.Warn("insecure (unpinned) download detected in %v: '%v'", pathfn, cmd) return true } @@ -328,7 +316,7 @@ func getRedirectFile(red []*syntax.Redirect) (string, bool) { } func isExecuteFiles(node syntax.Node, cmd, pathfn string, files map[string]bool, - cl checker.CheckLogger) bool { + dl checker.DetailLogger) bool { ce, ok := node.(*syntax.CallExpr) if !ok { return false @@ -342,8 +330,7 @@ func isExecuteFiles(node syntax.Node, cmd, pathfn string, files map[string]bool, ok = false for fn := range files { if isInterpreterWithFile(c, fn) || isExecuteFile(c, fn) { - cl.FailWithCode(binaryDownload, "%v is fetching and executing non-pinned program '%v'", - pathfn, cmd) + dl.Warn("insecure (unpinned) download detected in %v: '%v'", pathfn, cmd) ok = true } } @@ -485,7 +472,7 @@ func isPipUnpinnedDownload(cmd []string) bool { } func isUnpinnedPakageManagerDownload(node syntax.Node, cmd, pathfn string, - cl checker.CheckLogger) bool { + dl checker.DetailLogger) bool { ce, ok := node.(*syntax.CallExpr) if !ok { return false @@ -498,15 +485,13 @@ func isUnpinnedPakageManagerDownload(node syntax.Node, cmd, pathfn string, // Go get/install. if isGoUnpinnedDownload(c) { - cl.FailWithCode(packageInstall, "%v is fetching an non-pinned dependency '%v'", - pathfn, cmd) + dl.Warn("insecure (unpinned) download detected in %v: '%v'", pathfn, cmd) return true } // Pip install. if isPipUnpinnedDownload(c) { - cl.FailWithCode(packageInstall, "%v is fetching an non-pinned dependency '%v'", - pathfn, cmd) + dl.Warn("insecure (unpinned) download detected in %v: '%v'", pathfn, cmd) return true } @@ -539,7 +524,7 @@ func recordFetchFileFromNode(node syntax.Node) (pathfn string, ok bool, err erro } func isFetchProcSubsExecute(node syntax.Node, cmd, pathfn string, - cl checker.CheckLogger) bool { + dl checker.DetailLogger) bool { ce, ok := node.(*syntax.CallExpr) if !ok { return false @@ -589,8 +574,7 @@ func isFetchProcSubsExecute(node syntax.Node, cmd, pathfn string, return false } - cl.FailWithCode(binaryDownload, "%v is fetching and executing non-pinned program '%v'", - pathfn, cmd) + dl.Warn("insecure (unpinned) download detected in %v: '%v'", pathfn, cmd) return true } @@ -661,17 +645,17 @@ func nodeToString(p *syntax.Printer, node syntax.Node) (string, error) { err := p.Print(&buf, node) // This is ugly, but the parser does not have a defined error type :/. if err != nil && !strings.Contains(err.Error(), "unsupported node type") { - return "", fmt.Errorf("syntax.Printer.Print: %w", err) + return "", sce.Create(sce.ErrRunFailure, fmt.Sprintf("syntax.Printer.Print: %v", err)) } return buf.String(), nil } func validateShellFileAndRecord(pathfn string, content []byte, files map[string]bool, - cl checker.CheckLogger) (bool, error) { + dl checker.DetailLogger) (bool, error) { in := strings.NewReader(string(content)) f, err := syntax.NewParser().Parse(in, "") if err != nil { - return false, ErrParsingShellCommand + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidShellCode, err)) } printer := syntax.NewPrinter() @@ -688,7 +672,7 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string] c, ok := extractInterpreterCommandFromNode(node) // nolinter if ok { - ok, e := validateShellFileAndRecord(pathfn, []byte(c), files, cl) + ok, e := validateShellFileAndRecord(pathfn, []byte(c), files, dl) validated = ok if e != nil { err = e @@ -697,23 +681,23 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string] } // `curl | bash` (supports `sudo`). - if isFetchPipeExecute(node, cmdStr, pathfn, cl) { + if isFetchPipeExecute(node, cmdStr, pathfn, dl) { validated = false } // Check if we're calling a file we previously downloaded. // Includes `curl > /tmp/file [&&|;] [bash] /tmp/file` - if isExecuteFiles(node, cmdStr, pathfn, files, cl) { + if isExecuteFiles(node, cmdStr, pathfn, files, dl) { validated = false } // `bash <(wget -qO- http://website.com/my-script.sh)`. (supports `sudo`). - if isFetchProcSubsExecute(node, cmdStr, pathfn, cl) { + if isFetchProcSubsExecute(node, cmdStr, pathfn, dl) { validated = false } // Package manager's unpinned installs. - if isUnpinnedPakageManagerDownload(node, cmdStr, pathfn, cl) { + if isUnpinnedPakageManagerDownload(node, cmdStr, pathfn, dl) { validated = false } // TODO(laurent): add check for cat file | bash. @@ -789,9 +773,7 @@ func isShellScriptFile(pathfn string, content []byte) bool { return false } -func validateShellFile(pathfn string, content []byte, cl checker.CheckLogger) (bool, error) { +func validateShellFile(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { files := make(map[string]bool) - // TODO(laurent): add pass here for both BinaryDownload and packageInstall - // and remove from caller. - return validateShellFileAndRecord(pathfn, content, files, cl) + return validateShellFileAndRecord(pathfn, content, files, dl) } diff --git a/cmd/root.go b/cmd/root.go index 1a09a886854..7bf56267d67 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -51,6 +51,8 @@ var ( pypi string rubygems string showDetails bool + // UPGRADEv2: will be removed. + v2 bool // TODO(laurent): add explain command. // ErrorInvalidFormatFlag indicates an invalid option was passed for the 'format' argument. ErrorInvalidFormatFlag = errors.New("invalid format flag") @@ -158,7 +160,11 @@ or ./scorecard --{npm,pypi,rubgems}= [--checks=check1,...] [--show switch format { case formatDefault: - err = repoResult.AsString(showDetails, *logLevel, os.Stdout) + if v2 { + err = repoResult.AsString2(showDetails, *logLevel, os.Stdout) + } else { + err = repoResult.AsString(showDetails, *logLevel, os.Stdout) + } case formatCSV: err = repoResult.AsCSV(showDetails, *logLevel, os.Stdout) case formatJSON: @@ -307,6 +313,8 @@ func init() { rootCmd.Flags().StringSliceVar( &metaData, "metadata", []string{}, "metadata for the project.It can be multiple separated by commas") rootCmd.Flags().BoolVar(&showDetails, "show-details", false, "show extra details about each check") + // UPGRADEv2: will be removed. + rootCmd.Flags().BoolVar(&v2, "v2", false, "temporary flag to display v2 changes") checkNames := []string{} for checkName := range checks.AllChecks { checkNames = append(checkNames, checkName) diff --git a/errors/internal.go b/errors/internal.go new file mode 100644 index 00000000000..e7c8fd18212 --- /dev/null +++ b/errors/internal.go @@ -0,0 +1,28 @@ +// 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 errors + +import ( + "errors" +) + +//nolint +var ( + ErrInternalInvalidDockerFile = errors.New("invalid Dockerfile") + ErrInternalInvalidYamlFile = errors.New("invalid yaml file") + ErrInternalFilenameMatch = errors.New("filename match error") + ErrInternalEmptyFile = errors.New("empty file") + ErrInternalInvalidShellCode = errors.New("invalid shell code") +) diff --git a/errors/public.go b/errors/public.go new file mode 100644 index 00000000000..2dbdd3ebc78 --- /dev/null +++ b/errors/public.go @@ -0,0 +1,38 @@ +// 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 errors + +import ( + "errors" + "fmt" +) + +// UPGRADEv2: delete other files in folder. +//nolint +var ( + ErrRunFailure = errors.New("cannot run check") + ErrRepoUnreachable = errors.New("repo unreachable") +) + +// Create a public error using any of the errors +// listed above. Example: +func Create(e error, msg string) error { + // Note: Errorf automatically wraps the error when used with `%w`. + if len(msg) > 0 { + return fmt.Errorf("%w: %v", e, msg) + } + // We still need to use %w to prevent callers from using e == ErrInvalidDockerFile. + return fmt.Errorf("%w", e) +} diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index c6c176a7d83..7e36a89eb2e 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -25,7 +25,6 @@ import ( "strings" "github.com/olekukonko/tablewriter" - "github.com/ossf/scorecard/checker" "go.uber.org/zap/zapcore" ) @@ -90,7 +89,8 @@ func (r *ScorecardResult) AsCSV(showDetails bool, logLevel zapcore.Level, writer return nil } -func (r *ScorecardResult) AsString(showDetails bool, writer io.Writer) error { +// UPGRADEv2: will be removed. +func (r *ScorecardResult) AsString(showDetails bool, logLevel zapcore.Level, writer io.Writer) error { sortedChecks := make([]checker.CheckResult, len(r.Checks)) for i, checkResult := range r.Checks { sortedChecks[i] = checkResult @@ -118,7 +118,18 @@ func (r *ScorecardResult) AsString(showDetails bool, writer io.Writer) error { x[1] = strconv.Itoa(row.Confidence) x[2] = row.Name if showDetails { - x[3] = strings.Join(row.Details, "\n") + if row.Version == 2 { + sa := make([]string, 1) + for _, v := range row.Details2 { + if v.Type == checker.DetailDebug && logLevel != zapcore.DebugLevel { + continue + } + sa = append(sa, fmt.Sprintf("%s: %s", typeToString(v.Type), v.Msg)) + } + x[3] = strings.Join(sa, "\n") + } else { + x[3] = strings.Join(row.Details, "\n") + } } data[i] = x } @@ -136,25 +147,100 @@ func (r *ScorecardResult) AsString(showDetails bool, writer io.Writer) error { table.SetCenterSeparator("|") table.AppendBulk(data) table.Render() + return nil } -func displayResult(result bool) string { - if result { - return "Pass" +// UPGRADEv2: new code +func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, writer io.Writer) error { + sortedChecks := make([]checker.CheckResult, len(r.Checks)) + for i, checkResult := range r.Checks { + sortedChecks[i] = checkResult } - return "Fail" + sort.Slice(sortedChecks, func(i, j int) bool { + if sortedChecks[i].Pass == sortedChecks[j].Pass { + return sortedChecks[i].Name < sortedChecks[j].Name + } + return sortedChecks[i].Pass + }) + + data := make([][]string, len(sortedChecks)) + for i, row := range sortedChecks { + if row.Version != 2 { + continue + } + const withdetails = 5 + const withoutdetails = 4 + var x []string + + if showDetails { + x = make([]string, withdetails) + } else { + x = make([]string, withoutdetails) + } + + // UPGRADEv2: rename variable. + if row.Score2 == checker.InconclusiveResultScore { + x[0] = "Inconclusive" + } else { + x[0] = fmt.Sprintf("%d", row.Score2) + } + + doc := fmt.Sprintf("https://github.com/ossf/scorecard/blob/main/checks/checks.md#%s", strings.ToLower(row.Name)) + x[1] = row.Reason2 + x[2] = row.Name + if showDetails { + sa := make([]string, 1) + for _, v := range row.Details2 { + if v.Type == checker.DetailDebug && logLevel != zapcore.DebugLevel { + continue + } + sa = append(sa, fmt.Sprintf("%s: %s", typeToString(v.Type), v.Msg)) + } + x[3] = strings.Join(sa, "\n") + x[4] = doc + } else { + x[3] = doc + } + + data[i] = x + } + + table := tablewriter.NewWriter(os.Stdout) + header := []string{"Score", "Reason", "Name"} + if showDetails { + header = append(header, "Details") + } + header = append(header, "Documentation/Remdiation") + table.SetHeader(header) + table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + table.SetRowSeparator("-") + table.SetRowLine(true) + table.SetCenterSeparator("|") + table.AppendBulk(data) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetRowLine(true) + table.Render() + + return nil } -func displayResult2(result int) string { - switch result { +func typeToString(cd checker.DetailType) string { + switch cd { default: - panic("invalid result") - case checker.ResultPass: + panic("invalid detail") + case checker.DetailInfo: + return "Info" + case checker.DetailWarn: + return "Warn" + case checker.DetailDebug: + return "Debug" + } +} + +func displayResult(result bool) string { + if result { return "Pass" - case checker.ResultFail: - return "Fail" - case checker.ResultDontKnow: - return "N/A" } + return "Fail" } From 6adb82b7a621e085d8deb8ee3a5e9e2086fbe865 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Thu, 15 Jul 2021 19:32:23 +0000 Subject: [PATCH 12/31] typo --- pkg/scorecard_result.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index 7e36a89eb2e..561c9bd2491 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -211,7 +211,7 @@ func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, wr if showDetails { header = append(header, "Details") } - header = append(header, "Documentation/Remdiation") + header = append(header, "Documentation/Remediation") table.SetHeader(header) table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) table.SetRowSeparator("-") From c9cad02d9ebe95f72195263b50aad41e41acfbef Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Thu, 15 Jul 2021 20:09:47 +0000 Subject: [PATCH 13/31] create common unit test lib --- utests/utlib.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 utests/utlib.go diff --git a/utests/utlib.go b/utests/utlib.go new file mode 100644 index 00000000000..b5cd1eb47f5 --- /dev/null +++ b/utests/utlib.go @@ -0,0 +1,98 @@ +// 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 utests + +import ( + "errors" + "fmt" + "testing" + + "github.com/ossf/scorecard/checker" +) + +func validateDetailTypes(messages []checker.CheckDetail, nw, ni, nd int) bool { + enw := 0 + eni := 0 + end := 0 + for _, v := range messages { + switch v.Type { + default: + panic(fmt.Sprintf("invalid type %v", v.Type)) + case checker.DetailInfo: + eni += 1 + case checker.DetailDebug: + end += 1 + case checker.DetailWarn: + enw += 1 + } + } + return enw == nw && + eni == ni && + end == nd +} + +type TestDetailLogger struct { + messages []checker.CheckDetail +} + +type TestArgs struct { + Dl TestDetailLogger + Filename string +} + +type TestReturn struct { + Errors []error + Score int + NumberOfWarn int + NumberOfInfo int + NumberOfDebug int +} + +type TestInfo struct { + Args TestArgs + Expected TestReturn + Name string +} + +func (l *TestDetailLogger) Info(desc string, args ...interface{}) { + cd := checker.CheckDetail{Type: checker.DetailInfo, Msg: fmt.Sprintf(desc, args...)} + l.messages = append(l.messages, cd) +} + +func (l *TestDetailLogger) Warn(desc string, args ...interface{}) { + cd := checker.CheckDetail{Type: checker.DetailWarn, Msg: fmt.Sprintf(desc, args...)} + l.messages = append(l.messages, cd) +} + +func (l *TestDetailLogger) Debug(desc string, args ...interface{}) { + cd := checker.CheckDetail{Type: checker.DetailDebug, Msg: fmt.Sprintf(desc, args...)} + l.messages = append(l.messages, cd) +} + +func ValidateTest(t *testing.T, ti TestInfo, tr checker.CheckResult) { + for _, we := range ti.Expected.Errors { + if !errors.Is(tr.Error2, we) { + t.Errorf("TestDockerfileScriptDownload:\"%v\": invalid error returned: %v is not of type %v", + ti.Name, tr.Error, we) + } + } + // UPGRADEv2: update name. + if tr.Score2 != ti.Expected.Score || + !validateDetailTypes(ti.Args.Dl.messages, ti.Expected.NumberOfWarn, + ti.Expected.NumberOfInfo, ti.Expected.NumberOfDebug) { + t.Errorf("TestDockerfileScriptDownload:\"%v\": %v. Got (score=%v) expected (%v)\n%v", + ti.Name, ti.Args.Filename, tr.Score2, ti.Expected.Score, ti.Args.Dl.messages) + } +} From 18d413dd6ce497223c60e38ade64928e9d06692c Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Thu, 15 Jul 2021 22:46:00 +0000 Subject: [PATCH 14/31] added unit tests --- checker/check_runner.go | 26 +- checks/binary_artifact.go | 2 +- checks/frozen_deps.go | 97 +++- checks/frozen_deps_test.go | 585 ++++++++++------------- checks/testdata/Dockerfile-invalid | 22 +- checks/testdata/Dockerfile-not-pinned-as | 2 + utests/utlib.go | 3 +- 7 files changed, 354 insertions(+), 383 deletions(-) diff --git a/checker/check_runner.go b/checker/check_runner.go index 30a530bfba5..aef8a2743b0 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -55,6 +55,12 @@ type CheckDetail struct { Msg string // A short string explaining why the details was recorded/logged.. } +type DetailLogger interface { + Info(desc string, args ...interface{}) + Warn(desc string, args ...interface{}) + Debug(desc string, args ...interface{}) +} + // UPGRADEv2: messages2 will ultimately // be renamed to messages. type logger struct { @@ -62,23 +68,19 @@ type logger struct { messages2 []CheckDetail } -type DetailLogger struct { - l *logger -} - -func (l *DetailLogger) Info(desc string, args ...interface{}) { +func (l *logger) Info(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailInfo, Msg: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) + l.messages2 = append(l.messages2, cd) } -func (l *DetailLogger) Warn(desc string, args ...interface{}) { +func (l *logger) Warn(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailWarn, Msg: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) + l.messages2 = append(l.messages2, cd) } -func (l *DetailLogger) Debug(desc string, args ...interface{}) { +func (l *logger) Debug(desc string, args ...interface{}) { cd := CheckDetail{Type: DetailDebug, Msg: fmt.Sprintf(desc, args...)} - l.l.messages2 = append(l.l.messages2, cd) + l.messages2 = append(l.messages2, cd) } // UPGRADEv2: to remove. @@ -109,15 +111,13 @@ func (r *Runner) Run(ctx context.Context, f CheckFn) CheckResult { var res CheckResult var l logger - var dl DetailLogger for retriesRemaining := checkRetries; retriesRemaining > 0; retriesRemaining-- { checkRequest := r.CheckRequest checkRequest.Ctx = ctx l = logger{} - dl = DetailLogger{l: &l} // UPGRADEv2: to remove. checkRequest.Logf = l.Logf - checkRequest.Dlogger = dl + checkRequest.Dlogger = &l res = f(&checkRequest) // UPGRADEv2: to fix using proper error check. if res.ShouldRetry && !strings.Contains(res.Error.Error(), "invalid header field value") { diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 56e4ddfa357..6c9881b4c53 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -88,7 +88,7 @@ func checkBinaryFileContent(path string, content []byte, var t types.Type var err error if t, err = filetype.Get(content); err != nil { - return false, sce.Create(sce.ErrRunFailure, fmt.Sprint("filetype.Get:%v", err.Error())) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("filetype.Get:%v", err.Error())) } if _, ok := binaryFileTypes[t.Extension]; ok { diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index 2d86b8386dd..19a796c8c37 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -71,18 +71,30 @@ func FrozenDeps(c *checker.CheckRequest) checker.CheckResult { // TODO(laurent): need to support GCB pinning. func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { - r, err := CheckFilesContent2("*", false, c, validateShellScriptDownloads) + r, err := CheckFilesContent2("*", false, c, validateShellScriptIsFreeOfInsecureDownloads) + return createResultForIsShellScriptFreeOfInsecureDownloads(r, err) +} + +func createResultForIsShellScriptFreeOfInsecureDownloads(r bool, err error) checker.CheckResult { if err != nil { return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, "insecure (unpinned) dependency donwloads found in shell scripts") + return checker.CreateMinScoreResult(checkFrozenDeps, + "insecure (unpinned) dependency downloads found in shell scripts") } - return checker.CreateMaxScoreResult(checkFrozenDeps, "no insecure (unpinned) dependency downloads found in shell scripts") + return checker.CreateMaxScoreResult(checkFrozenDeps, + "no insecure (unpinned) dependency downloads found in shell scripts") +} + +func TestValidateShellScriptIsFreeOfInsecureDownloads(pathfn string, + content []byte, dl checker.DetailLogger) checker.CheckResult { + r, err := validateShellScriptIsFreeOfInsecureDownloads(pathfn, content, dl) + return createResultForIsShellScriptFreeOfInsecureDownloads(r, err) } -func validateShellScriptDownloads(pathfn string, content []byte, +func validateShellScriptIsFreeOfInsecureDownloads(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { // Validate the file type. if !isShellScriptFile(pathfn, content) { @@ -92,18 +104,31 @@ func validateShellScriptDownloads(pathfn string, content []byte, } func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { - r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfileDownloads) + r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfileIsFreeOfInsecureDownloads) + return createResultForIsDockerfileFreeOfInsecureDownloads(r, err) +} + +// Create the result. +func createResultForIsDockerfileFreeOfInsecureDownloads(r bool, err error) checker.CheckResult { if err != nil { return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, "insecure (unpinned) dependency donwloads found in Dockerfiles") + return checker.CreateMinScoreResult(checkFrozenDeps, + "insecure (unpinned) dependency downloads found in Dockerfiles") } - return checker.CreateMaxScoreResult(checkFrozenDeps, "no insecure (unpinned) dependency downloads found in Dockerfiles") + return checker.CreateMaxScoreResult(checkFrozenDeps, + "no insecure (unpinned) dependency downloads found in Dockerfiles") } -func validateDockerfileDownloads(pathfn string, content []byte, +func TestValidateDockerfileIsFreeOfInsecureDownloads(pathfn string, + content []byte, dl checker.DetailLogger) checker.CheckResult { + r, err := validateDockerfileIsFreeOfInsecureDownloads(pathfn, content, dl) + return createResultForIsDockerfileFreeOfInsecureDownloads(r, err) +} + +func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { contentReader := strings.NewReader(string(content)) res, err := parser.Parse(contentReader) @@ -140,18 +165,28 @@ func validateDockerfileDownloads(pathfn string, content []byte, } func isDockerfilePinned(c *checker.CheckRequest) checker.CheckResult { - r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfile) + r, err := CheckFilesContent2("*Dockerfile*", false, c, validateDockerfileIsPinned) + return createResultForIsDockerfilePinned(r, err) +} + +// Create the result. +func createResultForIsDockerfilePinned(r bool, err error) checker.CheckResult { if err != nil { return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } - if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, "unpinned dependencies found Dockerfiles") + if r { + return checker.CreateMaxScoreResult(checkFrozenDeps, "Dockerfile dependencies are pinned") } - return checker.CreateMaxScoreResult(checkFrozenDeps, "Dockerfile dependencies are pinned") + return checker.CreateMinScoreResult(checkFrozenDeps, "unpinned dependencies found Dockerfiles") } -func validateDockerfile(pathfn string, content []byte, +func TestValidateDockerfileIsPinned(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult { + r, err := validateDockerfileIsPinned(pathfn, content, dl) + return createResultForIsDockerfilePinned(r, err) +} + +func validateDockerfileIsPinned(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { // Users may use various names, e.g., // Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template @@ -230,18 +265,30 @@ func validateDockerfile(pathfn string, content []byte, } func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult { - r, err := CheckFilesContent2(".github/workflows/*", false, c, validateGitHubWorkflowShellScriptDownloads) + r, err := CheckFilesContent2(".github/workflows/*", false, c, validateGitHubWorkflowIsFreeOfInsecureDownloads) + return createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, err) +} + +// Create the result. +func createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r bool, err error) checker.CheckResult { if err != nil { return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, "insecure (unpinned) dependency donwloads found in GitHub workflows") + return checker.CreateMinScoreResult(checkFrozenDeps, + "insecure (unpinned) dependency donwloads found in GitHub workflows") } - return checker.CreateMaxScoreResult(checkFrozenDeps, "no insecure (unpinned) dependency downloads found in GitHub workflows") + return checker.CreateMaxScoreResult(checkFrozenDeps, + "no insecure (unpinned) dependency downloads found in GitHub workflows") +} + +func TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult { + r, err := validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn, content, dl) + return createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, err) } -func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, +func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { if len(content) == 0 { return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalEmptyFile.Error()) @@ -300,14 +347,24 @@ func validateGitHubWorkflowShellScriptDownloads(pathfn string, content []byte, // Check pinning of github actions in workflows. func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2(".github/workflows/*", true, c, validateGitHubActionWorkflow) + return createResultForIsGitHubActionsWorkflowPinned(r, err) +} + +// Create the result. +func createResultForIsGitHubActionsWorkflowPinned(r bool, err error) checker.CheckResult { if err != nil { return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) } - if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, "GitHub actions are not pinned") + if r { + return checker.CreateMaxScoreResult(checkFrozenDeps, "GitHub actions are pinned") } - return checker.CreateMinScoreResult(checkFrozenDeps, "GitHub actions are pinned") + return checker.CreateMinScoreResult(checkFrozenDeps, "GitHub actions are not pinned") +} + +func TestIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult { + r, err := validateGitHubActionWorkflow(pathfn, content, dl) + return createResultForIsGitHubActionsWorkflowPinned(r, err) } // Check file content. diff --git a/checks/frozen_deps_test.go b/checks/frozen_deps_test.go index b4c221148f4..3de603e8d41 100644 --- a/checks/frozen_deps_test.go +++ b/checks/frozen_deps_test.go @@ -15,525 +15,456 @@ package checks import ( - "errors" "fmt" "io/ioutil" - "strings" "testing" + + "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" + scut "github.com/ossf/scorecard/utests" ) +// TODO(laurent): share this function across unit tests + func TestGithubWorkflowPinning(t *testing.T) { t.Parallel() - //nolint - type args struct { - Log log - Filename string - } - type returnValue struct { - Error error - Result bool - NumberOfErrors int - } - - //nolint - tests := []struct { - args args - want returnValue - name string - }{ + tests := []scut.TestInfo{ { - name: "Zero size content", - args: args{ + Name: "Zero size content", + Args: scut.TestArgs{ Filename: "", - Log: log{}, }, - want: returnValue{ - Error: ErrEmptyFile, - Result: false, - NumberOfErrors: 0, + Expected: scut.TestReturn{ + Errors: []error{sce.ErrRunFailure}, + Score: checker.InconclusiveResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "Pinned workflow", - args: args{ + Name: "Pinned workflow", + Args: scut.TestArgs{ Filename: "./testdata/workflow-pinned.yaml", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: true, - NumberOfErrors: 0, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "Non-pinned workflow", - args: args{ + Name: "Non-pinned workflow", + Args: scut.TestArgs{ Filename: "./testdata/workflow-not-pinned.yaml", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 1, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 1, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, } - //nolint 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.Run(tt.Name, func(t *testing.T) { t.Parallel() var content []byte var err error - if tt.args.Filename == "" { + if tt.Args.Filename == "" { content = make([]byte, 0) } else { - content, err = ioutil.ReadFile(tt.args.Filename) + content, err = ioutil.ReadFile(tt.Args.Filename) if err != nil { panic(fmt.Errorf("cannot read file: %w", err)) } } - r, err := validateGitHubActionWorkflow(tt.args.Filename, content, tt.args.Log.Logf) - - if !errors.Is(err, tt.want.Error) || - r != tt.want.Result || - len(tt.args.Log.messages) != tt.want.NumberOfErrors { - t.Errorf("TestDockerfileScriptDownload:\"%v\": %v (%v,%v,%v) want (%v, %v, %v)\n%v", - tt.name, tt.args.Filename, r, err, len(tt.args.Log.messages), tt.want.Result, tt.want.Error, tt.want.NumberOfErrors, - strings.Join(tt.args.Log.messages, "\n")) - } + dl := scut.TestDetailLogger{} + r := TestIsGitHubActionsWorkflowPinned(tt.Args.Filename, content, &dl) + tt.Args.Dl = dl + scut.ValidateTest(t, &tt, r) }) } } func TestDockerfilePinning(t *testing.T) { t.Parallel() - type args struct { - Logf func(s string, f ...interface{}) - Filename string - } - - type returnValue struct { - Error error - Result bool - } - - l := log{} - tests := []struct { - args args - want returnValue - name string - }{ + tests := []scut.TestInfo{ { - name: "Invalid dockerfile", - args: args{ + Name: "Invalid dockerfile", + Args: scut.TestArgs{ Filename: "./testdata/Dockerfile-invalid", - Logf: l.Logf, }, - want: returnValue{ - Error: ErrInvalidDockerfile, - Result: false, + Expected: scut.TestReturn{ + Errors: []error{sce.ErrRunFailure}, + Score: checker.InconclusiveResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "Pinned dockerfile", - args: args{ + Name: "Pinned dockerfile", + Args: scut.TestArgs{ Filename: "./testdata/Dockerfile-pinned", - Logf: l.Logf, }, - want: returnValue{ - Error: nil, - Result: true, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "Pinned dockerfile as", - args: args{ + Name: "Pinned dockerfile as", + Args: scut.TestArgs{ Filename: "./testdata/Dockerfile-pinned-as", - Logf: l.Logf, }, - want: returnValue{ - Error: nil, - Result: true, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "Non-pinned dockerfile as", - args: args{ + Name: "Non-pinned dockerfile as", + Args: scut.TestArgs{ Filename: "./testdata/Dockerfile-not-pinned-as", - Logf: l.Logf, }, - want: returnValue{ - Error: nil, - Result: false, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 3, // TODO:fix should be 2 + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "Non-pinned dockerfile", - args: args{ + Name: "Non-pinned dockerfile", + Args: scut.TestArgs{ Filename: "./testdata/Dockerfile-not-pinned", - Logf: l.Logf, }, - want: returnValue{ - Error: nil, - Result: false, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 1, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, } for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below - l.messages = []string{} - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.Name, func(t *testing.T) { t.Parallel() var content []byte var err error - - content, err = ioutil.ReadFile(tt.args.Filename) - if err != nil { - panic(fmt.Errorf("cannot read file: %w", err)) - } - - r, err := validateDockerfile(tt.args.Filename, content, tt.args.Logf) - - if !errors.Is(err, tt.want.Error) || - r != tt.want.Result { - t.Errorf("TestGithubWorkflowPinning:\"%v\": %v (%v,%v) want (%v, %v)", - tt.name, tt.args.Filename, r, err, tt.want.Result, tt.want.Error) + if tt.Args.Filename == "" { + content = make([]byte, 0) + } else { + content, err = ioutil.ReadFile(tt.Args.Filename) + if err != nil { + panic(fmt.Errorf("cannot read file: %w", err)) + } } + dl := scut.TestDetailLogger{} + r := TestValidateDockerfileIsPinned(tt.Args.Filename, content, &dl) + tt.Args.Dl = dl + scut.ValidateTest(t, &tt, r) }) } } func TestDockerfileScriptDownload(t *testing.T) { t.Parallel() - //nolint - type args struct { - // Note: this seems to be defined in e2e/e2e_suite_test.go - Log log - Filename string - } - - type returnValue struct { - Error error - Result bool - NumberOfErrors int - } - - //nolint - tests := []struct { - args args - want returnValue - name string - }{ + tests := []scut.TestInfo{ { - name: "curl | sh", - args: args{ + Name: "curl | sh", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-curl-sh", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 4, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 4, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "wget | /bin/sh", - args: args{ + Name: "wget | /bin/sh", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-wget-bin-sh", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 3, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 3, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "wget no exec", - args: args{ + Name: "wget no exec", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-script-ok", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: true, - NumberOfErrors: 0, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "curl file sh", - args: args{ + Name: "curl file sh", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-curl-file-sh", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 12, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 12, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "proc substitution", - args: args{ + Name: "proc substitution", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-proc-subs", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 6, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 6, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "wget file", - args: args{ + Name: "wget file", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-wget-file", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 10, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 10, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "gsutil file", - args: args{ + Name: "gsutil file", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-gsutil-file", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 17, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 17, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "aws file", - args: args{ + Name: "aws file", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-aws-file", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 15, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 15, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "pkg managers", - args: args{ + Name: "pkg managers", + Args: scut.TestArgs{ Filename: "testdata/Dockerfile-pkg-managers", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 27, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 27, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, } - //nolint 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.Run(tt.Name, func(t *testing.T) { t.Parallel() var content []byte var err error - content, err = ioutil.ReadFile(tt.args.Filename) - if err != nil { - panic(fmt.Errorf("cannot read file: %w", err)) - } - - r, err := validateDockerfileDownloads(tt.args.Filename, content, tt.args.Log.Logf) - - if !errors.Is(err, tt.want.Error) || - r != tt.want.Result || - len(tt.args.Log.messages) != tt.want.NumberOfErrors { - t.Errorf("TestDockerfileScriptDownload:\"%v\": %v (%v,%v,%v) want (%v, %v, %v)\n%v", - tt.name, tt.args.Filename, r, err, len(tt.args.Log.messages), tt.want.Result, tt.want.Error, tt.want.NumberOfErrors, - strings.Join(tt.args.Log.messages, "\n")) + if tt.Args.Filename == "" { + content = make([]byte, 0) + } else { + content, err = ioutil.ReadFile(tt.Args.Filename) + if err != nil { + panic(fmt.Errorf("cannot read file: %w", err)) + } } + dl := scut.TestDetailLogger{} + r := TestValidateDockerfileIsFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) + tt.Args.Dl = dl + scut.ValidateTest(t, &tt, r) }) } } func TestShellScriptDownload(t *testing.T) { t.Parallel() - //nolint - type args struct { - // Note: this seems to be defined in e2e/e2e_suite_test.go - Log log - Filename string - } - - type returnValue struct { - Error error - Result bool - NumberOfErrors int - } - - //nolint - tests := []struct { - args args - want returnValue - name string - }{ + tests := []scut.TestInfo{ { - name: "sh script", - args: args{ + Name: "sh script", + Args: scut.TestArgs{ Filename: "testdata/script-sh", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 7, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 7, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "bash script", - args: args{ + Name: "bash script", + Args: scut.TestArgs{ Filename: "testdata/script-bash", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 7, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 7, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "sh script 2", - args: args{ + Name: "sh script 2", + Args: scut.TestArgs{ Filename: "testdata/script.sh", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 7, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 7, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "pkg managers", - args: args{ + Name: "pkg managers", + Args: scut.TestArgs{ Filename: "testdata/script-pkg-managers", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 24, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 24, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, } - //nolint 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.Run(tt.Name, func(t *testing.T) { t.Parallel() var content []byte var err error - content, err = ioutil.ReadFile(tt.args.Filename) - if err != nil { - panic(fmt.Errorf("cannot read file: %w", err)) - } - - r, err := validateShellScriptDownloads(tt.args.Filename, content, tt.args.Log.Logf) - - if !errors.Is(err, tt.want.Error) || - r != tt.want.Result || - len(tt.args.Log.messages) != tt.want.NumberOfErrors { - t.Errorf("TestDockerfileScriptDownload:\"%v\": %v (%v,%v,%v) want (%v, %v, %v)\n%v", - tt.name, tt.args.Filename, r, err, len(tt.args.Log.messages), tt.want.Result, tt.want.Error, tt.want.NumberOfErrors, - strings.Join(tt.args.Log.messages, "\n")) + if tt.Args.Filename == "" { + content = make([]byte, 0) + } else { + content, err = ioutil.ReadFile(tt.Args.Filename) + if err != nil { + panic(fmt.Errorf("cannot read file: %w", err)) + } } + dl := scut.TestDetailLogger{} + r := TestValidateShellScriptIsFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) + tt.Args.Dl = dl + scut.ValidateTest(t, &tt, r) }) } } func TestGitHubWorflowRunDownload(t *testing.T) { t.Parallel() - //nolint - type args struct { - // Note: this seems to be defined in e2e/e2e_suite_test.go - Log log - Filename string - } - - type returnValue struct { - Error error - Result bool - NumberOfErrors int - } - - //nolint - tests := []struct { - args args - want returnValue - name string - }{ + tests := []scut.TestInfo{ { - name: "workflow curl default", - args: args{ + Name: "workflow curl default", + Args: scut.TestArgs{ Filename: "testdata/github-workflow-curl-default", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 1, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 1, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "workflow curl no default", - args: args{ + Name: "workflow curl no default", + Args: scut.TestArgs{ Filename: "testdata/github-workflow-curl-no-default", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 1, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 1, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, { - name: "wget across steps", - args: args{ + Name: "wget across steps", + Args: scut.TestArgs{ Filename: "testdata/github-workflow-wget-across-steps", - Log: log{}, }, - want: returnValue{ - Error: nil, - Result: false, - NumberOfErrors: 2, + Expected: scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 2, + NumberOfInfo: 0, + NumberOfDebug: 0, }, }, } - //nolint 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.Run(tt.Name, func(t *testing.T) { t.Parallel() var content []byte var err error - content, err = ioutil.ReadFile(tt.args.Filename) - if err != nil { - panic(fmt.Errorf("cannot read file: %w", err)) - } - - r, err := validateGitHubWorkflowShellScriptDownloads(tt.args.Filename, content, tt.args.Log.Logf) - - if !errors.Is(err, tt.want.Error) || - r != tt.want.Result || - len(tt.args.Log.messages) != tt.want.NumberOfErrors { - t.Errorf("TestDockerfileScriptDownload:\"%v\": %v (%v,%v,%v) want (%v, %v, %v)\n%v", - tt.name, tt.args.Filename, r, err, len(tt.args.Log.messages), tt.want.Result, tt.want.Error, tt.want.NumberOfErrors, - strings.Join(tt.args.Log.messages, "\n")) + if tt.Args.Filename == "" { + content = make([]byte, 0) + } else { + content, err = ioutil.ReadFile(tt.Args.Filename) + if err != nil { + panic(fmt.Errorf("cannot read file: %w", err)) + } } + dl := scut.TestDetailLogger{} + r := TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) + tt.Args.Dl = dl + scut.ValidateTest(t, &tt, r) }) } } diff --git a/checks/testdata/Dockerfile-invalid b/checks/testdata/Dockerfile-invalid index 34d16b0231a..2f6d167136a 100644 --- a/checks/testdata/Dockerfile-invalid +++ b/checks/testdata/Dockerfile-invalid @@ -14,24 +14,4 @@ # limitations under the License. # Note: taken from https://github.com/pushiqiang/utils/blob/master/docker/Dockerfile_template -# 如果在中国,apt使用163源, ifconfig.co/json, http://ip-api.com -RUN curl -s ifconfig.co/json | grep "China" > /dev/null && \ - curl -s http://mirrors.163.com/.help/sources.list.jessie > /etc/apt/sources.list || true - -# 安装开发所需要的一些工具,同时方便在服务器上进行调试 -RUN apt-get update;\ - apt-get install -y vim;\ - true - -RUN mkdir /opt/somedir - -ENV PROJECT_NAME='test' -ENV PYTHONPATH="${PYTHONPATH}:/opt/somedir" - -COPY src/ /opt/somedir -WORKDIR /opt/somedir - -# 如果在中国,pip使用豆瓣源 -RUN curl -s ifconfig.co/json | grep "China" > /dev/null && \ - pip install -r requirements.txt -i https://pypi.doubanio.com/simple --trusted-host pypi.doubanio.com || \ - pip install -r requirements.txt \ No newline at end of file +#!/bin/sh diff --git a/checks/testdata/Dockerfile-not-pinned-as b/checks/testdata/Dockerfile-not-pinned-as index 8af7a8ba6a2..1a2f30c7e56 100644 --- a/checks/testdata/Dockerfile-not-pinned-as +++ b/checks/testdata/Dockerfile-not-pinned-as @@ -23,4 +23,6 @@ RUN CGO_ENABLED=0 make build-scorecard FROM build RUN /hello-world +FROM base + FROM python@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 \ No newline at end of file diff --git a/utests/utlib.go b/utests/utlib.go index b5cd1eb47f5..715108099ea 100644 --- a/utests/utlib.go +++ b/utests/utlib.go @@ -38,6 +38,7 @@ func validateDetailTypes(messages []checker.CheckDetail, nw, ni, nd int) bool { enw += 1 } } + return enw == nw && eni == ni && end == nd @@ -81,7 +82,7 @@ func (l *TestDetailLogger) Debug(desc string, args ...interface{}) { l.messages = append(l.messages, cd) } -func ValidateTest(t *testing.T, ti TestInfo, tr checker.CheckResult) { +func ValidateTest(t *testing.T, ti *TestInfo, tr checker.CheckResult) { for _, we := range ti.Expected.Errors { if !errors.Is(tr.Error2, we) { t.Errorf("TestDockerfileScriptDownload:\"%v\": invalid error returned: %v is not of type %v", From 752f6caeb4e58c61120deddfc8afbfe9094c5565 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Thu, 15 Jul 2021 23:36:40 +0000 Subject: [PATCH 15/31] add e2e tests --- checks/binary_artifact.go | 4 +- checks/frozen_deps_test.go | 12 +++-- e2e/automatic_deps_test.go | 40 ++++++++++++----- e2e/binary_artifacts_test.go | 85 ++++++++++++++++++++++++++++++++++++ e2e/code_review_test.go | 21 ++++++--- e2e/frozen_deps_test.go | 34 ++++++++++----- utests/utlib.go | 32 +++++++++++--- 7 files changed, 184 insertions(+), 44 deletions(-) create mode 100644 e2e/binary_artifacts_test.go diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 6c9881b4c53..ef4e5c14ceb 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -27,14 +27,14 @@ import ( //nolint func init() { - registerCheck(checkBinaryArtifacts, binaryArtifacts) + registerCheck(checkBinaryArtifacts, BinaryArtifacts) } // TODO: read the check code from file? const checkBinaryArtifacts string = "Binary-Artifacts" // BinaryArtifacts will check the repository if it contains binary artifacts. -func binaryArtifacts(c *checker.CheckRequest) checker.CheckResult { +func BinaryArtifacts(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*", false, c, checkBinaryFileContent) if err != nil { return checker.CreateRuntimeErrorResult(checkBinaryArtifacts, err) diff --git a/checks/frozen_deps_test.go b/checks/frozen_deps_test.go index 3de603e8d41..f2372dc1ff4 100644 --- a/checks/frozen_deps_test.go +++ b/checks/frozen_deps_test.go @@ -24,8 +24,6 @@ import ( scut "github.com/ossf/scorecard/utests" ) -// TODO(laurent): share this function across unit tests - func TestGithubWorkflowPinning(t *testing.T) { t.Parallel() @@ -87,7 +85,7 @@ func TestGithubWorkflowPinning(t *testing.T) { dl := scut.TestDetailLogger{} r := TestIsGitHubActionsWorkflowPinned(tt.Args.Filename, content, &dl) tt.Args.Dl = dl - scut.ValidateTest(t, &tt, r) + scut.ValidateTestInfo(t, &tt, &r) }) } } @@ -178,7 +176,7 @@ func TestDockerfilePinning(t *testing.T) { dl := scut.TestDetailLogger{} r := TestValidateDockerfileIsPinned(tt.Args.Filename, content, &dl) tt.Args.Dl = dl - scut.ValidateTest(t, &tt, r) + scut.ValidateTestInfo(t, &tt, &r) }) } } @@ -321,7 +319,7 @@ func TestDockerfileScriptDownload(t *testing.T) { dl := scut.TestDetailLogger{} r := TestValidateDockerfileIsFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) tt.Args.Dl = dl - scut.ValidateTest(t, &tt, r) + scut.ValidateTestInfo(t, &tt, &r) }) } } @@ -399,7 +397,7 @@ func TestShellScriptDownload(t *testing.T) { dl := scut.TestDetailLogger{} r := TestValidateShellScriptIsFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) tt.Args.Dl = dl - scut.ValidateTest(t, &tt, r) + scut.ValidateTestInfo(t, &tt, &r) }) } } @@ -464,7 +462,7 @@ func TestGitHubWorflowRunDownload(t *testing.T) { dl := scut.TestDetailLogger{} r := TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) tt.Args.Dl = dl - scut.ValidateTest(t, &tt, r) + scut.ValidateTestInfo(t, &tt, &r) }) } } diff --git a/e2e/automatic_deps_test.go b/e2e/automatic_deps_test.go index 22c81c99b60..98f974e09b4 100644 --- a/e2e/automatic_deps_test.go +++ b/e2e/automatic_deps_test.go @@ -23,47 +23,63 @@ import ( "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/checks" "github.com/ossf/scorecard/clients/githubrepo" + scut "github.com/ossf/scorecard/utests" ) +// TODO: use dedicated repo that don't change. +// TODO: need negative results var _ = Describe("E2E TEST:Automatic-Dependency-Update", func() { Context("E2E TEST:Validating dependencies are automatically updated", func() { It("Should return deps are automatically updated for dependabot", func() { - l := log{} + dl := scut.TestDetailLogger{} repoClient := githubrepo.CreateGithubRepoClient(context.Background(), ghClient) err := repoClient.InitRepo("ossf", "scorecard") Expect(err).Should(BeNil()) - checker := checker.CheckRequest{ + req := checker.CheckRequest{ Ctx: context.Background(), Client: ghClient, RepoClient: repoClient, Owner: "ossf", Repo: "scorecard", GraphClient: graphClient, - Logf: l.Logf, + Dlogger: &dl, } - result := checks.AutomaticDependencyUpdate(&checker) - Expect(result.Error).Should(BeNil()) - Expect(result.Pass).Should(BeTrue()) + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, + } + + result := checks.AutomaticDependencyUpdate(&req) + Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) It("Should return deps are automatically updated for renovatebot", func() { - l := log{} + dl := scut.TestDetailLogger{} repoClient := githubrepo.CreateGithubRepoClient(context.Background(), ghClient) err := repoClient.InitRepo("netlify", "netlify-cms") Expect(err).Should(BeNil()) - checker := checker.CheckRequest{ + req := checker.CheckRequest{ Ctx: context.Background(), Client: ghClient, RepoClient: repoClient, Owner: "netlify", Repo: "netlify-cms", GraphClient: graphClient, - Logf: l.Logf, + Dlogger: &dl, + } + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, } - result := checks.AutomaticDependencyUpdate(&checker) - Expect(result.Error).Should(BeNil()) - Expect(result.Pass).Should(BeTrue()) + result := checks.AutomaticDependencyUpdate(&req) + Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/e2e/binary_artifacts_test.go b/e2e/binary_artifacts_test.go new file mode 100644 index 00000000000..0f115e78825 --- /dev/null +++ b/e2e/binary_artifacts_test.go @@ -0,0 +1,85 @@ +// 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 e2e + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/ossf/scorecard/checker" + "github.com/ossf/scorecard/checks" + "github.com/ossf/scorecard/clients/githubrepo" + scut "github.com/ossf/scorecard/utests" +) + +// TODO: use dedicated repo that don't change. +// TODO: need negative results +var _ = Describe("E2E TEST:Binary-Artifacts", func() { + Context("E2E TEST:Binary artifacts are not present in source code", func() { + It("Should return not binary artifacts in source code", func() { + dl := scut.TestDetailLogger{} + repoClient := githubrepo.CreateGithubRepoClient(context.Background(), ghClient) + err := repoClient.InitRepo("ossf", "scorecard") + Expect(err).Should(BeNil()) + + req := checker.CheckRequest{ + Ctx: context.Background(), + Client: ghClient, + RepoClient: repoClient, + Owner: "ossf", + Repo: "scorecard", + GraphClient: graphClient, + Dlogger: &dl, + } + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, + } + + result := checks.BinaryArtifacts(&req) + Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + }) + It("Should return binary artifacts present in source code", func() { + dl := scut.TestDetailLogger{} + repoClient := githubrepo.CreateGithubRepoClient(context.Background(), ghClient) + err := repoClient.InitRepo("a1ive", "grub2-filemanager") + Expect(err).Should(BeNil()) + + req := checker.CheckRequest{ + Ctx: context.Background(), + Client: ghClient, + RepoClient: repoClient, + Owner: "a1ive", + Repo: "grub2-filemanager", + GraphClient: graphClient, + Dlogger: &dl, + } + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 1, + NumberOfInfo: 0, + NumberOfDebug: 0, + } + result := checks.BinaryArtifacts(&req) + Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + }) + }) +}) diff --git a/e2e/code_review_test.go b/e2e/code_review_test.go index 0c29eff7b9b..86ea5ad43c9 100644 --- a/e2e/code_review_test.go +++ b/e2e/code_review_test.go @@ -22,13 +22,16 @@ import ( "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/checks" + scut "github.com/ossf/scorecard/utests" ) +// TODO: use dedicated repo that don't change. +// TODO: need negative results var _ = Describe("E2E TEST:CodeReview", func() { Context("E2E TEST:Validating use of code reviews", func() { It("Should return use of code reviews", func() { - l := log{} - checkRequest := checker.CheckRequest{ + dl := scut.TestDetailLogger{} + req := checker.CheckRequest{ Ctx: context.Background(), Client: ghClient, HTTPClient: httpClient, @@ -36,11 +39,17 @@ var _ = Describe("E2E TEST:CodeReview", func() { Owner: "apache", Repo: "airflow", GraphClient: graphClient, - Logf: l.Logf, + Dlogger: &dl, } - result := checks.DoesCodeReview(&checkRequest) - Expect(result.Error).Should(BeNil()) - Expect(result.Pass).Should(BeTrue()) + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, + } + result := checks.DoesCodeReview(&req) + Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/e2e/frozen_deps_test.go b/e2e/frozen_deps_test.go index 3844ddc83b2..71d4bcd8c63 100644 --- a/e2e/frozen_deps_test.go +++ b/e2e/frozen_deps_test.go @@ -24,17 +24,20 @@ import ( "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/checks" "github.com/ossf/scorecard/clients/githubrepo" + scut "github.com/ossf/scorecard/utests" ) +// TODO: use dedicated repo that don't change. +// TODO: need negative results var _ = Describe("E2E TEST:FrozenDeps", func() { Context("E2E TEST:Validating deps are frozen", func() { It("Should return deps are not frozen", func() { - l := log{} + dl := scut.TestDetailLogger{} repoClient := githubrepo.CreateGithubRepoClient(context.Background(), ghClient) err := repoClient.InitRepo("tensorflow", "tensorflow") Expect(err).Should(BeNil()) - checkRequest := checker.CheckRequest{ + req := checker.CheckRequest{ Ctx: context.Background(), Client: ghClient, HTTPClient: httpClient, @@ -42,19 +45,25 @@ var _ = Describe("E2E TEST:FrozenDeps", func() { Owner: "tensorflow", Repo: "tensorflow", GraphClient: graphClient, - Logf: l.Logf, + Dlogger: &dl, } - result := checks.FrozenDeps(&checkRequest) - Expect(result.Error).Should(BeNil()) - Expect(result.Pass).Should(BeFalse()) + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, + } + result := checks.FrozenDeps(&req) + Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) It("Should return deps are frozen", func() { - l := log{} + dl := scut.TestDetailLogger{} repoClient := githubrepo.CreateGithubRepoClient(context.Background(), ghClient) err := repoClient.InitRepo("ossf", "scorecard") Expect(err).Should(BeNil()) - checkRequest := checker.CheckRequest{ + req := checker.CheckRequest{ Ctx: context.Background(), Client: ghClient, HTTPClient: httpClient, @@ -62,11 +71,12 @@ var _ = Describe("E2E TEST:FrozenDeps", func() { Owner: "ossf", Repo: "scorecard", GraphClient: graphClient, - Logf: l.Logf, + Dlogger: &dl, } - result := checks.FrozenDeps(&checkRequest) - Expect(result.Error).Should(BeNil()) - Expect(result.Pass).Should(BeTrue()) + result := checks.FrozenDeps(&req) + // Note: should be using ValidateTestReturn(). + Expect(result.Error2).Should(BeNil()) + Expect(result.Score2 == checker.MinResultScore).Should(BeTrue()) }) }) }) diff --git a/utests/utlib.go b/utests/utlib.go index 715108099ea..e28b99ee78d 100644 --- a/utests/utlib.go +++ b/utests/utlib.go @@ -82,18 +82,40 @@ func (l *TestDetailLogger) Debug(desc string, args ...interface{}) { l.messages = append(l.messages, cd) } -func ValidateTest(t *testing.T, ti *TestInfo, tr checker.CheckResult) { +func ValidateTestReturn(te *TestReturn, tr *checker.CheckResult, dl *TestDetailLogger) bool { + for _, we := range te.Errors { + if !errors.Is(tr.Error2, we) { + return false + } + } + // UPGRADEv2: update name. + if tr.Score2 != te.Score || + !validateDetailTypes(dl.messages, te.NumberOfWarn, + te.NumberOfInfo, te.NumberOfDebug) { + return false + } + return true +} + +func ValidateTestInfo(t *testing.T, ti *TestInfo, tr *checker.CheckResult) bool { for _, we := range ti.Expected.Errors { if !errors.Is(tr.Error2, we) { - t.Errorf("TestDockerfileScriptDownload:\"%v\": invalid error returned: %v is not of type %v", - ti.Name, tr.Error, we) + if t != nil { + t.Errorf("%v: invalid error returned: %v is not of type %v", + ti.Name, tr.Error, we) + } + return false } } // UPGRADEv2: update name. if tr.Score2 != ti.Expected.Score || !validateDetailTypes(ti.Args.Dl.messages, ti.Expected.NumberOfWarn, ti.Expected.NumberOfInfo, ti.Expected.NumberOfDebug) { - t.Errorf("TestDockerfileScriptDownload:\"%v\": %v. Got (score=%v) expected (%v)\n%v", - ti.Name, ti.Args.Filename, tr.Score2, ti.Expected.Score, ti.Args.Dl.messages) + if t != nil { + t.Errorf("%v: %v. Got (score=%v) expected (%v)\n%v", + ti.Name, ti.Args.Filename, tr.Score2, ti.Expected.Score, ti.Args.Dl.messages) + } + return false } + return true } From f20d1e0d25ed7d156ee937ac3f4cc4bea09e3271 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 00:21:10 +0000 Subject: [PATCH 16/31] add readmes --- CONTRIBUTING.md | 22 ++++++---------------- checks/write.md | 31 +++++++++++++++++++++++++++++++ errors/errors.md | 28 ++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 checks/write.md create mode 100644 errors/errors.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3fd55feedc..fbdc49ed10e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,7 @@ project. This document describes the contribution guidelines for the project. * [Getting started](#getting-started) * [Environment Setup](#environment-setup) * [Contributing steps](#contributing-steps) +* [Error handling](#error-handling) * [How to build scorecard locally](#how-to-build-scorecard-locally) * [PR Process](#pr-process) * [What to do before submitting a pull request](#what-to-do-before-submitting-a-pull-request) @@ -52,6 +53,10 @@ You must install these tools: 1. Fork the desired repo, develop and test your code changes. 1. Submit a pull request. +## Error handling + +See [errors/errors.md]. + ## How to build scorecard locally Note that, by building the scorecard from the source code we are allowed to test @@ -140,22 +145,7 @@ Submit a PR for this file and scorecard would start scanning in subsequent runs. ## Adding New Checks -Each check is currently just a function of type `CheckFn`. The signature is: - -```golang -type CheckFn func(*c.Checker) CheckResult -``` - -Checks are registered in an init function: - -```golang -const codeReviewStr = "Code-Review" - -//nolint:gochecknoinits -func init() { - registerCheck(codeReviewStr, DoesCodeReview) -} -``` +See [checks/write.md](checks/write.md). ### Updating Docs diff --git a/checks/write.md b/checks/write.md new file mode 100644 index 00000000000..d7501368610 --- /dev/null +++ b/checks/write.md @@ -0,0 +1,31 @@ +# How to write a check +The steps to writting a check are as follow: + +1. Create a file under checks, say `checks/mycheck.go` +2. Decide on a name, register the check: +``` +// Note: do not export the name: start its name with a lower-case letter. +const checkMyChech string = "My-Check" + +func init() { + registerCheck(checkBinaryArtifacts, BinaryArtifacts) +} +``` +3. Log information that is benfical to the user using `checker.DetailLogger`: + * Use `checker.DetailLogger.Warn()` to provide detail on low-score results. This is showed when the user supplies the `show-results` option. + * Use `checker.DetailLogger.Info()` to provide detail on high-score results. This is showed when the user supplies the `show-results` option. + * Use `checker.DetailLogger.Debug()` to provide detail on in verbose mode: this is showed only when the user supplies the `--verbosity Debug` option. +4. If the checks fails to run in a way that is irrecoverable, use `checker.CreateRuntimeErrorResult()` function. An exmple of this is if an error is returned from an API you call. +5. Create the result of the check as follow: + * Always provide a high-level sentence explaining the result/score of the check. + * If the check runs properly but is unable to conclude babout the score, use `checker.CreateInconclusiveResult()` function. + * For propertional results, use `checker.CreateProportionalScoreResult()`. + * For maximum score, use `checker.CreateMaxScoreResult()`; for min score use `checker.CreateMinScoreResult()` + * If you need to set your score yourself, use `checker.CreateResultWithScore()` with one of the constants declared, such as checker.HalfResultScore. + -- +6. Dealing with errors: see [../errors/errors.md](errors/errors/md). +7. Create unit tests for both low, high and inconclusive score. Put them in a file `checks/mycheck_test.go` +8. Create e2e tests in `e2e/mycheck_test.go`. Use a dedicated repo whereata will not change over time, so that it's reliable for the tests. +9. Update the `checks/checks.yaml` with the description of your check. +10. Gerenate the `checks/check.md` using `go build && cd checks/main && ./main`. Verity `checks/check.md` was updated. +10. Update the [README.md](https://github.com/ossf/scorecard#scorecard-checks) with a short description of your check. diff --git a/errors/errors.md b/errors/errors.md new file mode 100644 index 00000000000..c737ba3f62f --- /dev/null +++ b/errors/errors.md @@ -0,0 +1,28 @@ +# How to handle errors + +```golang +import sce "github.com/ossf/scorecard/errors" + +// Public errors are defined in errors/public.go and are exposed to callers. +// Internal errors are defined in errors/innternal.go. Their names start with ErrInternalXXX + +// Examples: + +// Return a standard check run failure, with an error message from an internal error. +// We only create internal errors for errors that may happen in several places in the code: this provides +// consistent erorr messages to the caller. +return sce.Create(sce.ErrRunFailure, ErrInternalInvalidYamlFile.Error()) + +// Return a standard check run failure, with an error message from an internal error and and API call error. +err := dependency.apiCall() +if err != nil { + return sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalSomething, err)) +} + +// Return a standard check run failure, only with API call error. Use this format when there is no internal error associated +// to the failure. In many cases, we don't need internal errors. +err := dependency.apiCall() +if err != nil { + return sce.Create(sce.ErrRunFailure, fmt.Sprintf("dependency.apiCall: %v", err)) +} +``` \ No newline at end of file From 2ee92b9a7219dadf2c2a8074413cdb97c6367339 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 01:22:23 +0000 Subject: [PATCH 17/31] fix e2e tests --- checker/check_result.go | 2 +- checks/frozen_deps.go | 2 +- e2e/automatic_deps_test.go | 16 +++++++++++++--- e2e/binary_artifacts_test.go | 14 ++++++++++++-- e2e/code_review_test.go | 9 +++++++-- e2e/executable_test.go | 1 + e2e/frozen_deps_test.go | 27 +++++++++++++++++++++------ pkg/scorecard_result.go | 2 +- utests/utlib.go | 3 ++- 9 files changed, 59 insertions(+), 17 deletions(-) diff --git a/checker/check_result.go b/checker/check_result.go index 5c4f7ad5477..16ce5f8df61 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -109,7 +109,7 @@ func CreateProportionalScoreResult(name, reason string, b, t int) CheckResult { Version: 2, Error2: nil, Score2: 10 * b / t, - Reason2: fmt.Sprintf("%v -- code normalized to %d", reason, score), + Reason2: fmt.Sprintf("%v -- score normalized to %d", reason, score), } } diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index 19a796c8c37..13ce3fbfbac 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -58,7 +58,7 @@ func init() { // FrozenDeps will check the repository if it contains frozen dependecies. func FrozenDeps(c *checker.CheckRequest) checker.CheckResult { - return checker.MultiCheckAnd( + return checker.MultiCheckAnd2( isPackageManagerLockFilePresent, isGitHubActionsWorkflowPinned, isDockerfilePinned, diff --git a/e2e/automatic_deps_test.go b/e2e/automatic_deps_test.go index 98f974e09b4..4e489e4e29c 100644 --- a/e2e/automatic_deps_test.go +++ b/e2e/automatic_deps_test.go @@ -27,7 +27,7 @@ import ( ) // TODO: use dedicated repo that don't change. -// TODO: need negative results +// TODO: need negative results. var _ = Describe("E2E TEST:Automatic-Dependency-Update", func() { Context("E2E TEST:Validating dependencies are automatically updated", func() { It("Should return deps are automatically updated for dependabot", func() { @@ -49,11 +49,16 @@ var _ = Describe("E2E TEST:Automatic-Dependency-Update", func() { Errors: nil, Score: checker.MaxResultScore, NumberOfWarn: 0, - NumberOfInfo: 0, + NumberOfInfo: 1, NumberOfDebug: 0, } result := checks.AutomaticDependencyUpdate(&req) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeTrue()) + // New version. Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) It("Should return deps are automatically updated for renovatebot", func() { @@ -75,10 +80,15 @@ var _ = Describe("E2E TEST:Automatic-Dependency-Update", func() { Errors: nil, Score: checker.MaxResultScore, NumberOfWarn: 0, - NumberOfInfo: 0, + NumberOfInfo: 1, NumberOfDebug: 0, } result := checks.AutomaticDependencyUpdate(&req) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeTrue()) + // New version. Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) }) diff --git a/e2e/binary_artifacts_test.go b/e2e/binary_artifacts_test.go index 0f115e78825..059d3d4ba4b 100644 --- a/e2e/binary_artifacts_test.go +++ b/e2e/binary_artifacts_test.go @@ -27,7 +27,7 @@ import ( ) // TODO: use dedicated repo that don't change. -// TODO: need negative results +// TODO: need negative results. var _ = Describe("E2E TEST:Binary-Artifacts", func() { Context("E2E TEST:Binary artifacts are not present in source code", func() { It("Should return not binary artifacts in source code", func() { @@ -54,6 +54,11 @@ var _ = Describe("E2E TEST:Binary-Artifacts", func() { } result := checks.BinaryArtifacts(&req) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeTrue()) + // New version. Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) It("Should return binary artifacts present in source code", func() { @@ -73,12 +78,17 @@ var _ = Describe("E2E TEST:Binary-Artifacts", func() { } expected := scut.TestReturn{ Errors: nil, - Score: checker.MaxResultScore, + Score: checker.MinResultScore, NumberOfWarn: 1, NumberOfInfo: 0, NumberOfDebug: 0, } result := checks.BinaryArtifacts(&req) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeFalse()) + // New version. Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) }) diff --git a/e2e/code_review_test.go b/e2e/code_review_test.go index 86ea5ad43c9..13ac7412eb0 100644 --- a/e2e/code_review_test.go +++ b/e2e/code_review_test.go @@ -26,7 +26,7 @@ import ( ) // TODO: use dedicated repo that don't change. -// TODO: need negative results +// TODO: need negative results. var _ = Describe("E2E TEST:CodeReview", func() { Context("E2E TEST:Validating use of code reviews", func() { It("Should return use of code reviews", func() { @@ -46,9 +46,14 @@ var _ = Describe("E2E TEST:CodeReview", func() { Score: checker.MaxResultScore, NumberOfWarn: 0, NumberOfInfo: 0, - NumberOfDebug: 0, + NumberOfDebug: 30, } result := checks.DoesCodeReview(&req) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeTrue()) + // New version. Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) }) diff --git a/e2e/executable_test.go b/e2e/executable_test.go index a3158d0b656..2f6f4ec62c4 100644 --- a/e2e/executable_test.go +++ b/e2e/executable_test.go @@ -48,6 +48,7 @@ var _ = Describe("E2E TEST:executable", func() { Expect(len(data.MetaData)).ShouldNot(BeZero()) Expect(data.MetaData[0]).Should(BeEquivalentTo("openssf")) + // UPGRADEv2: TBD. for _, c := range data.Checks { switch c.CheckName { case "Active": diff --git a/e2e/frozen_deps_test.go b/e2e/frozen_deps_test.go index 71d4bcd8c63..690cbd089e2 100644 --- a/e2e/frozen_deps_test.go +++ b/e2e/frozen_deps_test.go @@ -28,7 +28,7 @@ import ( ) // TODO: use dedicated repo that don't change. -// TODO: need negative results +// TODO: need negative results. var _ = Describe("E2E TEST:FrozenDeps", func() { Context("E2E TEST:Validating deps are frozen", func() { It("Should return deps are not frozen", func() { @@ -49,12 +49,17 @@ var _ = Describe("E2E TEST:FrozenDeps", func() { } expected := scut.TestReturn{ Errors: nil, - Score: checker.MinResultScore, - NumberOfWarn: 0, + Score: checker.InconclusiveResultScore, + NumberOfWarn: 219, NumberOfInfo: 0, NumberOfDebug: 0, } result := checks.FrozenDeps(&req) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeFalse()) + // New version. Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) It("Should return deps are frozen", func() { @@ -73,10 +78,20 @@ var _ = Describe("E2E TEST:FrozenDeps", func() { GraphClient: graphClient, Dlogger: &dl, } + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 1, + NumberOfDebug: 0, + } result := checks.FrozenDeps(&req) - // Note: should be using ValidateTestReturn(). - Expect(result.Error2).Should(BeNil()) - Expect(result.Score2 == checker.MinResultScore).Should(BeTrue()) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeTrue()) + // New version. + Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index 561c9bd2491..bcc9fa78564 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -167,7 +167,7 @@ func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, wr data := make([][]string, len(sortedChecks)) for i, row := range sortedChecks { if row.Version != 2 { - continue + panic("wrong version") } const withdetails = 5 const withoutdetails = 4 diff --git a/utests/utlib.go b/utests/utlib.go index e28b99ee78d..b669e7fe61a 100644 --- a/utests/utlib.go +++ b/utests/utlib.go @@ -38,7 +38,6 @@ func validateDetailTypes(messages []checker.CheckDetail, nw, ni, nd int) bool { enw += 1 } } - return enw == nw && eni == ni && end == nd @@ -85,6 +84,8 @@ func (l *TestDetailLogger) Debug(desc string, args ...interface{}) { func ValidateTestReturn(te *TestReturn, tr *checker.CheckResult, dl *TestDetailLogger) bool { for _, we := range te.Errors { if !errors.Is(tr.Error2, we) { + fmt.Printf("invalid error returned: %v is not of type %v", + tr.Error, we) return false } } From 81ce38387fda637261f08e06e52b650d9e73c917 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 01:24:51 +0000 Subject: [PATCH 18/31] fix --- cron/worker/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cron/worker/main.go b/cron/worker/main.go index 0a3e5467ad2..cf5de25b5d9 100644 --- a/cron/worker/main.go +++ b/cron/worker/main.go @@ -89,7 +89,7 @@ func processRequest(ctx context.Context, return fmt.Errorf("error during RunScorecards: %w", err) } result.Date = batchRequest.GetJobTime().AsTime().Format("2006-01-02") - if err := result.AsJSON(true /*showDetails*/, &buffer); err != nil { + if err := result.AsJSON(true /*showDetails*/, zapcore.InfoLevel, &buffer); err != nil { return fmt.Errorf("error during result.AsJSON: %w", err) } } From 1e9b27e7f448c1e0db7ca1cdc9a0e646f7fbdf9a Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 01:53:52 +0000 Subject: [PATCH 19/31] linter --- checker/check_runner.go | 3 ++- checks/binary_artifact.go | 3 ++- checks/checkforcontent.go | 9 ++++++--- checks/code_review.go | 11 ++++------- checks/frozen_deps.go | 19 ++++++++++++++----- checks/shell_download_validate.go | 16 +++++++++++----- cron/worker/main.go | 1 + e2e/frozen_deps_test.go | 2 +- e2e/security_policy_test.go | 2 +- utests/utlib.go | 11 ++++++----- 10 files changed, 48 insertions(+), 29 deletions(-) diff --git a/checker/check_runner.go b/checker/check_runner.go index aef8a2743b0..cb16afe7ed2 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -146,7 +146,8 @@ func Bool2int(b bool) int { // UPGRADEv2: will be renamed. func MultiCheckOr2(fns ...CheckFn) CheckFn { return func(c *CheckRequest) CheckResult { - var maxResult CheckResult //{Version:2} + //nolint + maxResult := CheckResult{Version: 2} for _, fn := range fns { result := fn(c) diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index ef4e5c14ceb..290adda7c41 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -88,7 +88,8 @@ func checkBinaryFileContent(path string, content []byte, var t types.Type var err error if t, err = filetype.Get(content); err != nil { - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("filetype.Get:%v", err.Error())) + //nolint + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("filetype.Get:%s", err.Error())) } if _, ok := binaryFileTypes[t.Extension]; ok { diff --git a/checks/checkforcontent.go b/checks/checkforcontent.go index 03a13b80cdc..2d46312af9a 100644 --- a/checks/checkforcontent.go +++ b/checks/checkforcontent.go @@ -34,13 +34,15 @@ func isMatchingPath(pattern, fullpath string, caseSensitive bool) (bool, error) filename := path.Base(fullpath) match, err := path.Match(pattern, fullpath) if err != nil { - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalFilenameMatch, err)) + //nolint + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalFilenameMatch.Error(), err.Error())) } // No match on the fullpath, let's try on the filename only. if !match { if match, err = path.Match(pattern, filename); err != nil { - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalFilenameMatch, err)) + //nolint + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalFilenameMatch.Error(), err.Error())) } } @@ -125,7 +127,8 @@ func CheckFilesContent2(shellPathFnPattern string, for _, file := range c.RepoClient.ListFiles(predicate) { content, err := c.RepoClient.GetFileContent(file) if err != nil { - return false, err + //nolint + return false, sce.Create(sce.ErrRunFailure, err.Error()) } rr, err := onFileContent(file, content, c.Dlogger) diff --git a/checks/code_review.go b/checks/code_review.go index 81c8f1df4a2..9a9e5abacdb 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -28,7 +28,6 @@ import ( const ( // checkCodeReview is the registered name for DoesCodeReview. checkCodeReview = "Code-Review" - crPassThreshold = .75 pullRequestsToAnalyze = 30 reviewsToAnalyze = 30 labelsToAnalyze = 30 @@ -131,7 +130,7 @@ func githubCodeReview(c *checker.CheckRequest) checker.CheckResult { } } - return createResult(c, "GitHub", totalReviewed, totalMerged) + return createResult("GitHub", totalReviewed, totalMerged) } func isPrReviewRequired(c *checker.CheckRequest) checker.CheckResult { @@ -162,7 +161,7 @@ func prowCodeReview(c *checker.CheckRequest) checker.CheckResult { } } - return createResult(c, "Prow", totalReviewed, totalMerged) + return createResult("Prow", totalReviewed, totalMerged) } func commitMessageHints(c *checker.CheckRequest) checker.CheckResult { @@ -198,15 +197,13 @@ func commitMessageHints(c *checker.CheckRequest) checker.CheckResult { } } - return createResult(c, "Gerrit", totalReviewed, total) + return createResult("Gerrit", totalReviewed, total) } -func createResult(c *checker.CheckRequest, reviewName string, reviewed, total int) checker.CheckResult { +func createResult(reviewName string, reviewed, total int) checker.CheckResult { if total > 0 { reason := fmt.Sprintf("%s code reviews found for %v commits out of the last %v", reviewName, reviewed, total) return checker.CreateProportionalScoreResult(checkCodeReview, reason, reviewed, total) - } - return checker.CreateInconclusiveResult(checkCodeReview, fmt.Sprintf("no %s commits found", reviewName)) } diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index 13ce3fbfbac..f42fa4c9d96 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -133,7 +133,8 @@ func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte, contentReader := strings.NewReader(string(content)) res, err := parser.Parse(contentReader) if err != nil { - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidDockerFile, err)) + //nolint + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalInvalidDockerFile.Error(), err.Error())) } // nolint: prealloc @@ -153,6 +154,7 @@ func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte, } if len(valueList) == 0 { + //nolint return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalInvalidDockerFile.Error()) } @@ -202,7 +204,8 @@ func validateDockerfileIsPinned(pathfn string, content []byte, pinnedAsNames := make(map[string]bool) res, err := parser.Parse(contentReader) if err != nil { - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidDockerFile, err)) + //nolint + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalInvalidDockerFile.Error(), err.Error())) } for _, child := range res.AST.Children { @@ -252,12 +255,14 @@ func validateDockerfileIsPinned(pathfn string, content []byte, default: // That should not happen. + //nolint return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalInvalidDockerFile.Error()) } } // The file should have at least one FROM statement. if !fromFound { + //nolint return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalInvalidDockerFile.Error()) } @@ -283,7 +288,8 @@ func createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r bool, err er "no insecure (unpinned) dependency downloads found in GitHub workflows") } -func TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult { +func TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string, + content []byte, dl checker.DetailLogger) checker.CheckResult { r, err := validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn, content, dl) return createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, err) } @@ -297,8 +303,9 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by var workflow gitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { + //nolint return false, sce.Create(sce.ErrRunFailure, - fmt.Sprintf("%v:%v:%v:%v", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) + fmt.Sprintf("%s:%s:%s:%s", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) } githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`) @@ -370,14 +377,16 @@ func TestIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker // Check file content. func validateGitHubActionWorkflow(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { if len(content) == 0 { + //nolint return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalEmptyFile.Error()) } var workflow gitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { + //nolint return false, sce.Create(sce.ErrRunFailure, - fmt.Sprintf("%v:%v:%v:%v", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) + fmt.Sprintf("%s:%s:%s:%s", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) } hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`) diff --git a/checks/shell_download_validate.go b/checks/shell_download_validate.go index cb9138e7d9e..1f6f8aaebee 100644 --- a/checks/shell_download_validate.go +++ b/checks/shell_download_validate.go @@ -101,7 +101,8 @@ func getWgetOutputFile(cmd []string) (pathfn string, ok bool, err error) { u, err := url.Parse(cmd[i]) if err != nil { - return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) + //nolint + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %s", err.Error())) } return path.Base(u.Path), true, nil } @@ -120,7 +121,8 @@ func getGsutilOutputFile(cmd []string) (pathfn string, ok bool, err error) { // Directory. u, err := url.Parse(cmd[i]) if err != nil { - return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) + //nolint + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %s", err.Error())) } return filepath.Join(filepath.Dir(pathfn), path.Base(u.Path)), true, nil } @@ -145,7 +147,8 @@ func getAWSOutputFile(cmd []string) (pathfn string, ok bool, err error) { if filepath.Clean(filepath.Dir(ofile)) == filepath.Clean(ofile) { u, err := url.Parse(ifile) if err != nil { - return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) + //nolint + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %s", err.Error())) } return filepath.Join(filepath.Dir(ofile), path.Base(u.Path)), true, nil } @@ -645,7 +648,8 @@ func nodeToString(p *syntax.Printer, node syntax.Node) (string, error) { err := p.Print(&buf, node) // This is ugly, but the parser does not have a defined error type :/. if err != nil && !strings.Contains(err.Error(), "unsupported node type") { - return "", sce.Create(sce.ErrRunFailure, fmt.Sprintf("syntax.Printer.Print: %v", err)) + //nolint + return "", sce.Create(sce.ErrRunFailure, fmt.Sprintf("syntax.Printer.Print: %s", err.Error())) } return buf.String(), nil } @@ -655,7 +659,9 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string] in := strings.NewReader(string(content)) f, err := syntax.NewParser().Parse(in, "") if err != nil { - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidShellCode, err)) + //nolint + return false, sce.Create(sce.ErrRunFailure, + fmt.Sprintf("%s: %s", sce.ErrInternalInvalidShellCode.Error(), err.Error())) } printer := syntax.NewPrinter() diff --git a/cron/worker/main.go b/cron/worker/main.go index cf5de25b5d9..d8aabfe0ea7 100644 --- a/cron/worker/main.go +++ b/cron/worker/main.go @@ -29,6 +29,7 @@ import ( "github.com/shurcooL/githubv4" "go.opencensus.io/stats/view" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/checks" diff --git a/e2e/frozen_deps_test.go b/e2e/frozen_deps_test.go index 690cbd089e2..c5a9155d59a 100644 --- a/e2e/frozen_deps_test.go +++ b/e2e/frozen_deps_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//nolint: dupl // repeating test cases that are slightly different is acceptable +//nolint: dupl package e2e import ( diff --git a/e2e/security_policy_test.go b/e2e/security_policy_test.go index 7ac87afdff2..afca2e8fa15 100644 --- a/e2e/security_policy_test.go +++ b/e2e/security_policy_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//nolint:dupl // repeating test cases that are slightly different is acceptable +//nolint:dupl package e2e import ( diff --git a/utests/utlib.go b/utests/utlib.go index b669e7fe61a..c5a204a41bd 100644 --- a/utests/utlib.go +++ b/utests/utlib.go @@ -31,11 +31,11 @@ func validateDetailTypes(messages []checker.CheckDetail, nw, ni, nd int) bool { default: panic(fmt.Sprintf("invalid type %v", v.Type)) case checker.DetailInfo: - eni += 1 + eni++ case checker.DetailDebug: - end += 1 + end++ case checker.DetailWarn: - enw += 1 + enw++ } } return enw == nw && @@ -48,8 +48,8 @@ type TestDetailLogger struct { } type TestArgs struct { - Dl TestDetailLogger Filename string + Dl TestDetailLogger } type TestReturn struct { @@ -61,9 +61,9 @@ type TestReturn struct { } type TestInfo struct { + Name string Args TestArgs Expected TestReturn - Name string } func (l *TestDetailLogger) Info(desc string, args ...interface{}) { @@ -98,6 +98,7 @@ func ValidateTestReturn(te *TestReturn, tr *checker.CheckResult, dl *TestDetailL return true } +//nolint func ValidateTestInfo(t *testing.T, ti *TestInfo, tr *checker.CheckResult) bool { for _, we := range ti.Expected.Errors { if !errors.Is(tr.Error2, we) { From 9acdbc5348fca7df72fc54f03bef9c82c13823e8 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 01:57:47 +0000 Subject: [PATCH 20/31] fix --- pkg/scorecard_result.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index bcc9fa78564..561c9bd2491 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -167,7 +167,7 @@ func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, wr data := make([][]string, len(sortedChecks)) for i, row := range sortedChecks { if row.Version != 2 { - panic("wrong version") + continue } const withdetails = 5 const withoutdetails = 4 From 8a1749f69707c52b2e958c22b2c130d997be5b94 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 17:59:35 +0000 Subject: [PATCH 21/31] fixes --- checks/checks2.yaml | 87 ++++++++++++++++++--------------------------- checks/write.md | 4 +-- errors/public.go | 2 +- 3 files changed, 37 insertions(+), 56 deletions(-) diff --git a/checks/checks2.yaml b/checks/checks2.yaml index e06809efdcd..8b4726c62d8 100644 --- a/checks/checks2.yaml +++ b/checks/checks2.yaml @@ -16,34 +16,51 @@ # Run `cd checks/main && go run /main` to generate `checks.json` and `checks.md`. checks: Binary-Artifacts: + risk: High description: >- This check tries to determine if a project has binary artifacts in the source repository. - These binaries could be compromised artifacts. Building from the source is recommended. + + Binaries are a threat to auditability and vulnerability management. + In addition, a binary could be compromised or malicious. + A low score is therefore considered `High` risk. remediation: - >- Remove the binary artifacts from the repository. + - >- + Build from source. Automatic-Dependency-Update: + risk: High description: >- This check tries to determine if a project has dependencies automatically updated. + + Not updating dependencies makes a project vulnerable to known flaws and prone to attacks. + A low score is therefore considered `High` risk. + The checks looks for [dependabot](https://dependabot.com/docs/config-file/) or [renovatebot](https://docs.renovatebot.com/configuration-options/). This check only looks if it is enabled and does not ensure that it is run and pull requests are merged. + remediation: - >- Signup for automatic dependency updates with dependabot or renovatebot and place the config file in the locations that are recommended by these tools. Code-Review: + risk: High description: >- This check tries to determine if a project requires code review before - pull requests are merged. First it checks if branch-Protection is enabled + pull requests are merged. + + Reviewing code improves quality of code in general. In addition, it ensures + compromised contributors cannot intentionally inject malicious code. A low + score is therefore considered `High` risk. + + The check first tries to detect if branch-Protection is enabled on the default branch and the number of reviewers is at least 1. If this - fails, it checks if the recent (~30) commits have a Github-approved + fails, it checks if the recent (~30) commits have a Github-approved review or if the merger is different from the committer (implicit review). - The check succeeds if at least 75% of commits have a review as described - above. If it fails, it does the same check but looking for reviews by + It also performs similar check for reviews using [Prow](https://github.com/kubernetes/test-infra/tree/master/prow#readme) - (labels "lgtm" or "approved"). If this fails, it does the same but looking - for gerrit-specific commit messages ("Reviewed-on" and "Reviewed-by"). + (labels "lgtm" or "approved") and Gerrit ("Reviewed-on" and "Reviewed-by"). remediation: - >- Follow security best practices by performing strict code reviews for @@ -55,16 +72,21 @@ checks: Enforce the rule for administrators / code owners as well. E.g. [GitHub](https://docs.github.com/en/github/administering-a-repository/about-protected-branches#include-administrators) Frozen-Deps: + risk: Medium description: >- This check tries to determine if a project has declared and pinned its - dependencies. It works by (1) looking for the following files in the root + dependencies. + + Pinning dependencies is important to mitigate compromised dependencies + from undermining the security of the project. Low score is therefore considered + `Medium` risk. + + The checks works by (1) looking for the following files in the root directory: go.mod, go.sum (Golang), package-lock.json, npm-shrinkwrap.json (Javascript), requirements.txt, pipfile.lock (Python), gemfile.lock (Ruby), cargo.lock (Rust), yarn.lock (package manager), composer.lock (PHP), vendor/, third_party/, third-party/; (2) looks for - unpinned dependencies in Dockerfiles, shell scripts and GitHub workflows. If one of - the files in (1) AND all the dependencies in (2) are pinned, the check - succeds. + unpinned dependencies in Dockerfiles, shell scripts and GitHub workflows. remediation: - >- Declare all your dependencies with specific versions in your package @@ -83,45 +105,4 @@ checks: - >- To help update your dependencies after pinning them, use tools such as Github's [dependabot](https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/) - or [renovate bot](https://github.com/renovatebot/renovate). - failures: - LockFile: - description: >- - No lock file is found in the root direory of the repo. - remediation: - - >- - Declare all your dependencies with specific versions in your package - format file (e.g. `package.json` for npm, `requirements.txt` for - python). For C/C++, check in the code from a trusted source and add a - `README` on the specific version used (and the archive SHA hashes). - - >- - If the package manager supports lock files (e.g. `package-lock.json` for - npm), make sure to check these in the source code as well. These files - maintain signatures for the entire dependency tree and saves from future - exploitation in case the package is compromised. - GitHubActions: - description: >- - GitHub workflows use non-pinned dependencies. - remediation: - - >- - pin dependencies by hash. See example - [gitcache-docker.yaml](https://github.com/ossf/scorecard/blob/main/.github/workflows/gitcache-docker.yaml#L36) - BinaryDownload: - description: >- - GitHub workflows, Dockerfiles or shell scripts download binaries from the Internet. - remediation: - - >- - Build from source. For shell scripts, commimt to the repo or use [sget](https://blog.sigstore.dev/a-safer-curl-bash-7698c8125063) - and pin by hash. - Dockerfile: - description: >- - Dockerfile does not pin its dependencies by has in `FROM`. - remediation: - - >- - Pin dependencies by hash. See [Dockerfile](https://github.com/ossf/scorecard/blob/main/cron/worker/Dockerfile) examples. - PackageInstall: - description: >- - Package managers should command should pin packages they install. - remediation: - - >- - For golang, `go install pkg@hash`. For an example, see [TODO]() + or [renovate bot](https://github.com/renovatebot/renovate). \ No newline at end of file diff --git a/checks/write.md b/checks/write.md index d7501368610..1bffa3cd9c3 100644 --- a/checks/write.md +++ b/checks/write.md @@ -21,11 +21,11 @@ func init() { * If the check runs properly but is unable to conclude babout the score, use `checker.CreateInconclusiveResult()` function. * For propertional results, use `checker.CreateProportionalScoreResult()`. * For maximum score, use `checker.CreateMaxScoreResult()`; for min score use `checker.CreateMinScoreResult()` - * If you need to set your score yourself, use `checker.CreateResultWithScore()` with one of the constants declared, such as checker.HalfResultScore. + * If you need more flexibility and need to set a specific score, use `checker.CreateResultWithScore()` with one of the constants declared, such as `checker.HalfResultScore`. -- 6. Dealing with errors: see [../errors/errors.md](errors/errors/md). 7. Create unit tests for both low, high and inconclusive score. Put them in a file `checks/mycheck_test.go` 8. Create e2e tests in `e2e/mycheck_test.go`. Use a dedicated repo whereata will not change over time, so that it's reliable for the tests. 9. Update the `checks/checks.yaml` with the description of your check. -10. Gerenate the `checks/check.md` using `go build && cd checks/main && ./main`. Verity `checks/check.md` was updated. +10. Gerenate the `checks/check.md` using `go build && cd checks/main && ./main`. Verify `checks/check.md` was updated. 10. Update the [README.md](https://github.com/ossf/scorecard#scorecard-checks) with a short description of your check. diff --git a/errors/public.go b/errors/public.go index 2dbdd3ebc78..c0fc4ba5ef7 100644 --- a/errors/public.go +++ b/errors/public.go @@ -27,7 +27,7 @@ var ( ) // Create a public error using any of the errors -// listed above. Example: +// listed above. For examples, see errors/errors.md. func Create(e error, msg string) error { // Note: Errorf automatically wraps the error when used with `%w`. if len(msg) > 0 { From c1a7d3505b632b8f598a0f9444f94fa250433d6a Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 18:20:02 +0000 Subject: [PATCH 22/31] nits --- checks/binary_artifact.go | 2 +- checks/checkforcontent.go | 4 ++-- checks/frozen_deps.go | 11 ++++++----- checks/permissions.go | 1 + checks/shell_download_validate.go | 10 +++++----- e2e/frozen_deps_test.go | 1 - e2e/security_policy_test.go | 1 - pkg/scorecard_result.go | 4 +++- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 290adda7c41..2f66ee4b222 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -89,7 +89,7 @@ func checkBinaryFileContent(path string, content []byte, var err error if t, err = filetype.Get(content); err != nil { //nolint - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("filetype.Get:%s", err.Error())) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("filetype.Get:%v", err)) } if _, ok := binaryFileTypes[t.Extension]; ok { diff --git a/checks/checkforcontent.go b/checks/checkforcontent.go index 2d46312af9a..3d0c219a9cb 100644 --- a/checks/checkforcontent.go +++ b/checks/checkforcontent.go @@ -35,14 +35,14 @@ func isMatchingPath(pattern, fullpath string, caseSensitive bool) (bool, error) match, err := path.Match(pattern, fullpath) if err != nil { //nolint - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalFilenameMatch.Error(), err.Error())) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalFilenameMatch, err)) } // No match on the fullpath, let's try on the filename only. if !match { if match, err = path.Match(pattern, filename); err != nil { //nolint - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalFilenameMatch.Error(), err.Error())) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalFilenameMatch, err)) } } diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index f42fa4c9d96..b6614ec164b 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -134,7 +134,7 @@ func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte, res, err := parser.Parse(contentReader) if err != nil { //nolint - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalInvalidDockerFile.Error(), err.Error())) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidDockerFile, err)) } // nolint: prealloc @@ -205,7 +205,7 @@ func validateDockerfileIsPinned(pathfn string, content []byte, res, err := parser.Parse(contentReader) if err != nil { //nolint - return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%s: %s", sce.ErrInternalInvalidDockerFile.Error(), err.Error())) + return false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalInvalidDockerFile, err)) } for _, child := range res.AST.Children { @@ -281,7 +281,7 @@ func createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r bool, err er } if !r { return checker.CreateMinScoreResult(checkFrozenDeps, - "insecure (unpinned) dependency donwloads found in GitHub workflows") + "insecure (unpinned) dependency downloads found in GitHub workflows") } return checker.CreateMaxScoreResult(checkFrozenDeps, @@ -297,6 +297,7 @@ func TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string, func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) { if len(content) == 0 { + //nolint return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalEmptyFile.Error()) } @@ -305,7 +306,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by if err != nil { //nolint return false, sce.Create(sce.ErrRunFailure, - fmt.Sprintf("%s:%s:%s:%s", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) + fmt.Sprintf("%v:%s:%s:%v", sce.ErrInternalInvalidYamlFile, pathfn, string(content), err)) } githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`) @@ -386,7 +387,7 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, dl checker.Deta if err != nil { //nolint return false, sce.Create(sce.ErrRunFailure, - fmt.Sprintf("%s:%s:%s:%s", sce.ErrInternalInvalidYamlFile.Error(), pathfn, string(content), err.Error())) + fmt.Sprintf("%v:%s:%s:%v", sce.ErrInternalInvalidYamlFile, pathfn, string(content), err)) } hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`) diff --git a/checks/permissions.go b/checks/permissions.go index 8a538f82320..5e2bc1dae13 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -122,6 +122,7 @@ func validateReadPermissions(config map[interface{}]interface{}, path string, func validateGitHubActionTokenPermissions(path string, content []byte, logf func(s string, f ...interface{})) (bool, error) { if len(content) == 0 { + //nolint return false, sce.Create(sce.ErrRunFailure, sce.ErrInternalEmptyFile.Error()) } diff --git a/checks/shell_download_validate.go b/checks/shell_download_validate.go index 1f6f8aaebee..a14b8ab6b21 100644 --- a/checks/shell_download_validate.go +++ b/checks/shell_download_validate.go @@ -102,7 +102,7 @@ func getWgetOutputFile(cmd []string) (pathfn string, ok bool, err error) { u, err := url.Parse(cmd[i]) if err != nil { //nolint - return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %s", err.Error())) + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) } return path.Base(u.Path), true, nil } @@ -122,7 +122,7 @@ func getGsutilOutputFile(cmd []string) (pathfn string, ok bool, err error) { u, err := url.Parse(cmd[i]) if err != nil { //nolint - return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %s", err.Error())) + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) } return filepath.Join(filepath.Dir(pathfn), path.Base(u.Path)), true, nil } @@ -148,7 +148,7 @@ func getAWSOutputFile(cmd []string) (pathfn string, ok bool, err error) { u, err := url.Parse(ifile) if err != nil { //nolint - return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %s", err.Error())) + return "", false, sce.Create(sce.ErrRunFailure, fmt.Sprintf("url.Parse: %v", err)) } return filepath.Join(filepath.Dir(ofile), path.Base(u.Path)), true, nil } @@ -649,7 +649,7 @@ func nodeToString(p *syntax.Printer, node syntax.Node) (string, error) { // This is ugly, but the parser does not have a defined error type :/. if err != nil && !strings.Contains(err.Error(), "unsupported node type") { //nolint - return "", sce.Create(sce.ErrRunFailure, fmt.Sprintf("syntax.Printer.Print: %s", err.Error())) + return "", sce.Create(sce.ErrRunFailure, fmt.Sprintf("syntax.Printer.Print: %v", err)) } return buf.String(), nil } @@ -661,7 +661,7 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string] if err != nil { //nolint return false, sce.Create(sce.ErrRunFailure, - fmt.Sprintf("%s: %s", sce.ErrInternalInvalidShellCode.Error(), err.Error())) + fmt.Sprintf("%v: %v", sce.ErrInternalInvalidShellCode, err)) } printer := syntax.NewPrinter() diff --git a/e2e/frozen_deps_test.go b/e2e/frozen_deps_test.go index c5a9155d59a..b96a5b3ad3a 100644 --- a/e2e/frozen_deps_test.go +++ b/e2e/frozen_deps_test.go @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//nolint: dupl package e2e import ( diff --git a/e2e/security_policy_test.go b/e2e/security_policy_test.go index afca2e8fa15..d943c6a86df 100644 --- a/e2e/security_policy_test.go +++ b/e2e/security_policy_test.go @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//nolint:dupl package e2e import ( diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index 561c9bd2491..fc7ee302247 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -118,6 +118,7 @@ func (r *ScorecardResult) AsString(showDetails bool, logLevel zapcore.Level, wri x[1] = strconv.Itoa(row.Confidence) x[2] = row.Name if showDetails { + //nolint if row.Version == 2 { sa := make([]string, 1) for _, v := range row.Details2 { @@ -151,7 +152,7 @@ func (r *ScorecardResult) AsString(showDetails bool, logLevel zapcore.Level, wri return nil } -// UPGRADEv2: new code +// UPGRADEv2: new code. func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, writer io.Writer) error { sortedChecks := make([]checker.CheckResult, len(r.Checks)) for i, checkResult := range r.Checks { @@ -166,6 +167,7 @@ func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, wr data := make([][]string, len(sortedChecks)) for i, row := range sortedChecks { + //nolint if row.Version != 2 { continue } From 95a1df699a2eaf2d58c9a41341e6b30f650219d5 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 18:21:56 +0000 Subject: [PATCH 23/31] doc --- checks/write.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checks/write.md b/checks/write.md index 1bffa3cd9c3..47b3bff5a1a 100644 --- a/checks/write.md +++ b/checks/write.md @@ -29,3 +29,5 @@ func init() { 9. Update the `checks/checks.yaml` with the description of your check. 10. Gerenate the `checks/check.md` using `go build && cd checks/main && ./main`. Verify `checks/check.md` was updated. 10. Update the [README.md](https://github.com/ossf/scorecard#scorecard-checks) with a short description of your check. + +For actual examples, look at [checks/binary_artifact.go](binary_artifact.go), [checks/code_review.go](code_review.go) and [checks/frozen_deps.go](frozen_deps.go). \ No newline at end of file From fdad3c8d01f6fbe4e448174b9f961009296cae51 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 20:25:34 +0000 Subject: [PATCH 24/31] linter --- checker/check_result.go | 8 ++++++-- checker/check_runner.go | 4 ++-- checker/check_test.go | 2 +- checks/active.go | 1 - checks/binary_artifact.go | 1 - checks/branch_protected.go | 1 - checks/branch_protected_test.go | 2 +- checks/ci_tests.go | 1 - checks/code_review.go | 3 +-- checks/contributors.go | 1 - checks/fuzzing.go | 1 - checks/packaging.go | 1 - checks/permissions.go | 3 +-- checks/pull_requests.go | 1 - checks/sast.go | 1 - checks/signed_releases.go | 1 - checks/signed_tags.go | 3 +-- checks/vulnerabilities.go | 1 - e2e/automatic_deps_test.go | 1 + e2e/binary_artifacts_test.go | 1 + pkg/scorecard.go | 7 +++---- pkg/scorecard_result.go | 17 +++++++++++++++-- 22 files changed, 33 insertions(+), 29 deletions(-) diff --git a/checker/check_result.go b/checker/check_result.go index 16ce5f8df61..8f7d6e46b0f 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -27,8 +27,8 @@ const ( MinResultConfidence = 0 ) -// UPGRADEv2: to remove. // ErrorDemoninatorZero indicates the denominator for a proportional result is 0. +// UPGRADEv2: to remove. var ErrorDemoninatorZero = errors.New("internal error: denominator is 0") //nolint @@ -172,6 +172,8 @@ func MakeAndResult2(checks ...CheckResult) CheckResult { worseResult := checks[0] + // UPGRADEv2: will go away after old struct is removed. + //nolint for _, result := range checks[1:] { if result.Score2 < worseResult.Score2 { worseResult = result @@ -248,6 +250,7 @@ func MakeProportionalResult(name string, numerator int, denominator int, } // Given a min result, check if another result is worse. +//nolint func isMinResult(result, min CheckResult) bool { if Bool2int(result.Pass) < Bool2int(min.Pass) { return true @@ -267,7 +270,8 @@ func MakeAndResult(checks ...CheckResult) CheckResult { Pass: true, Confidence: MaxResultConfidence, } - + // UPGRADEv2: will go away after old struct is removed. + //nolint for _, result := range checks { if minResult.Name == "" { minResult.Name = result.Name diff --git a/checker/check_runner.go b/checker/check_runner.go index cb16afe7ed2..08855e9a10f 100644 --- a/checker/check_runner.go +++ b/checker/check_runner.go @@ -88,7 +88,7 @@ func (l *logger) Logf(s string, f ...interface{}) { l.messages = append(l.messages, fmt.Sprintf(s, f...)) } -func logStats(ctx context.Context, startTime time.Time, result CheckResult) error { +func logStats(ctx context.Context, startTime time.Time, result *CheckResult) error { runTimeInSecs := time.Now().Unix() - startTime.Unix() opencensusstats.Record(ctx, stats.CheckRuntimeInSec.M(runTimeInSecs)) @@ -130,7 +130,7 @@ func (r *Runner) Run(ctx context.Context, f CheckFn) CheckResult { res.Details = l.messages res.Details2 = l.messages2 - if err := logStats(ctx, startTime, res); err != nil { + if err := logStats(ctx, startTime, &res); err != nil { panic(err) } return res diff --git a/checker/check_test.go b/checker/check_test.go index 99ad6f6fa80..b0e18517cbd 100644 --- a/checker/check_test.go +++ b/checker/check_test.go @@ -27,8 +27,8 @@ func TestMakeCheckAnd(t *testing.T) { t.Parallel() tests := []struct { name string - checks []CheckResult want CheckResult + checks []CheckResult }{ { name: "Multiple passing", diff --git a/checks/active.go b/checks/active.go index f57781698ba..89f88b89329 100644 --- a/checks/active.go +++ b/checks/active.go @@ -18,7 +18,6 @@ import ( "time" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 2f66ee4b222..57dfb3575cf 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -20,7 +20,6 @@ import ( "github.com/h2non/filetype" "github.com/h2non/filetype/types" - "github.com/ossf/scorecard/checker" sce "github.com/ossf/scorecard/errors" ) diff --git a/checks/branch_protected.go b/checks/branch_protected.go index 286eb1b5371..3129cfd140f 100644 --- a/checks/branch_protected.go +++ b/checks/branch_protected.go @@ -20,7 +20,6 @@ import ( "regexp" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/branch_protected_test.go b/checks/branch_protected_test.go index be5845c4b45..0285759daa8 100644 --- a/checks/branch_protected_test.go +++ b/checks/branch_protected_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) @@ -88,6 +87,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock rel1 := "release/v.1" sha := "8fb3cb86082b17144a80402f5367ae65f06083bd" main := "main" + //nolint tests := []struct { name string branches []*string diff --git a/checks/ci_tests.go b/checks/ci_tests.go index 87073b290c2..b96c6d9f55e 100644 --- a/checks/ci_tests.go +++ b/checks/ci_tests.go @@ -19,7 +19,6 @@ import ( "strings" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/code_review.go b/checks/code_review.go index 9a9e5abacdb..cd7c145f593 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -20,9 +20,8 @@ import ( "strings" "github.com/google/go-github/v32/github" - "github.com/shurcooL/githubv4" - "github.com/ossf/scorecard/checker" + "github.com/shurcooL/githubv4" ) const ( diff --git a/checks/contributors.go b/checks/contributors.go index 7a4d1b32d00..eb2c8812808 100644 --- a/checks/contributors.go +++ b/checks/contributors.go @@ -18,7 +18,6 @@ import ( "strings" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/fuzzing.go b/checks/fuzzing.go index 84ac24e6495..a77cb887a3d 100644 --- a/checks/fuzzing.go +++ b/checks/fuzzing.go @@ -18,7 +18,6 @@ import ( "fmt" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/packaging.go b/checks/packaging.go index 4bbcad7052a..db05a40e97e 100644 --- a/checks/packaging.go +++ b/checks/packaging.go @@ -20,7 +20,6 @@ import ( "strings" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/permissions.go b/checks/permissions.go index 5e2bc1dae13..e8987362eae 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -19,10 +19,9 @@ import ( "fmt" "strings" - "gopkg.in/yaml.v2" - "github.com/ossf/scorecard/checker" sce "github.com/ossf/scorecard/errors" + "gopkg.in/yaml.v2" ) const CheckPermissions = "Token-Permissions" diff --git a/checks/pull_requests.go b/checks/pull_requests.go index 5f9fc1f96b2..cdc77cf1fa3 100644 --- a/checks/pull_requests.go +++ b/checks/pull_requests.go @@ -18,7 +18,6 @@ import ( "strings" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/sast.go b/checks/sast.go index 72b22185004..9ede7663930 100644 --- a/checks/sast.go +++ b/checks/sast.go @@ -18,7 +18,6 @@ import ( "errors" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/signed_releases.go b/checks/signed_releases.go index 6d4873697e9..f2bd4a31e76 100644 --- a/checks/signed_releases.go +++ b/checks/signed_releases.go @@ -19,7 +19,6 @@ import ( "strings" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/checks/signed_tags.go b/checks/signed_tags.go index 85bcd6428c7..4dee6dd8eef 100644 --- a/checks/signed_tags.go +++ b/checks/signed_tags.go @@ -17,9 +17,8 @@ package checks import ( "errors" - "github.com/shurcooL/githubv4" - "github.com/ossf/scorecard/checker" + "github.com/shurcooL/githubv4" ) const ( diff --git a/checks/vulnerabilities.go b/checks/vulnerabilities.go index a9453992ae9..71cb48a8a80 100644 --- a/checks/vulnerabilities.go +++ b/checks/vulnerabilities.go @@ -22,7 +22,6 @@ import ( "strings" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" ) diff --git a/e2e/automatic_deps_test.go b/e2e/automatic_deps_test.go index 4e489e4e29c..32ec51e69ed 100644 --- a/e2e/automatic_deps_test.go +++ b/e2e/automatic_deps_test.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//nolint:dupl package e2e import ( diff --git a/e2e/binary_artifacts_test.go b/e2e/binary_artifacts_test.go index 059d3d4ba4b..63940d9f73e 100644 --- a/e2e/binary_artifacts_test.go +++ b/e2e/binary_artifacts_test.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//nolint:dupl package e2e import ( diff --git a/pkg/scorecard.go b/pkg/scorecard.go index d712532f1d1..f2ef43f41d6 100644 --- a/pkg/scorecard.go +++ b/pkg/scorecard.go @@ -22,14 +22,13 @@ import ( "time" "github.com/google/go-github/v32/github" - "github.com/shurcooL/githubv4" - opencensusstats "go.opencensus.io/stats" - "go.opencensus.io/tag" - "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/clients" "github.com/ossf/scorecard/repos" "github.com/ossf/scorecard/stats" + "github.com/shurcooL/githubv4" + opencensusstats "go.opencensus.io/stats" + "go.opencensus.io/tag" ) func logStats(ctx context.Context, startTime time.Time) { diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index fc7ee302247..5be53601a8d 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -51,6 +51,8 @@ func (r *ScorecardResult) AsJSON(showDetails bool, logLevel zapcore.Level, write Date: r.Date, Metadata: r.Metadata, } + // UPGRADEv2: remove nolint after uggrade. + //nolint for _, checkResult := range r.Checks { tmpResult := checker.CheckResult{ Name: checkResult.Name, @@ -69,6 +71,8 @@ func (r *ScorecardResult) AsCSV(showDetails bool, logLevel zapcore.Level, writer w := csv.NewWriter(writer) record := []string{r.Repo} columns := []string{"Repository"} + // UPGRADEv2: remove nolint after uggrade. + //nolint for _, checkResult := range r.Checks { columns = append(columns, checkResult.Name+"_Pass", checkResult.Name+"_Confidence") record = append(record, strconv.FormatBool(checkResult.Pass), @@ -92,6 +96,7 @@ func (r *ScorecardResult) AsCSV(showDetails bool, logLevel zapcore.Level, writer // UPGRADEv2: will be removed. func (r *ScorecardResult) AsString(showDetails bool, logLevel zapcore.Level, writer io.Writer) error { sortedChecks := make([]checker.CheckResult, len(r.Checks)) + //nolint for i, checkResult := range r.Checks { sortedChecks[i] = checkResult } @@ -103,6 +108,7 @@ func (r *ScorecardResult) AsString(showDetails bool, logLevel zapcore.Level, wri }) data := make([][]string, len(sortedChecks)) + //nolint for i, row := range sortedChecks { const withdetails = 4 const withoutdetails = 3 @@ -120,7 +126,7 @@ func (r *ScorecardResult) AsString(showDetails bool, logLevel zapcore.Level, wri if showDetails { //nolint if row.Version == 2 { - sa := make([]string, 1) + var sa []string for _, v := range row.Details2 { if v.Type == checker.DetailDebug && logLevel != zapcore.DebugLevel { continue @@ -155,6 +161,8 @@ func (r *ScorecardResult) AsString(showDetails bool, logLevel zapcore.Level, wri // UPGRADEv2: new code. func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, writer io.Writer) error { sortedChecks := make([]checker.CheckResult, len(r.Checks)) + //nolint + // UPGRADEv2: not needed after upgrade. for i, checkResult := range r.Checks { sortedChecks[i] = checkResult } @@ -166,6 +174,8 @@ func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, wr }) data := make([][]string, len(sortedChecks)) + //nolint + // UPGRADEv2: not needed after upgrade. for i, row := range sortedChecks { //nolint if row.Version != 2 { @@ -192,7 +202,9 @@ func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, wr x[1] = row.Reason2 x[2] = row.Name if showDetails { - sa := make([]string, 1) + // UPGRADEv2: change to make([]string, len(row.Details)) + // followed by sa[i] = instead of append + var sa []string for _, v := range row.Details2 { if v.Type == checker.DetailDebug && logLevel != zapcore.DebugLevel { continue @@ -240,6 +252,7 @@ func typeToString(cd checker.DetailType) string { } } +// UPGRADEv2: not needed after upgrade. func displayResult(result bool) string { if result { return "Pass" From f831bfb2579002d6bffd4d2306fecae7c7f82e4b Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 20:33:58 +0000 Subject: [PATCH 25/31] linter --- checks/active.go | 1 + checks/binary_artifact.go | 1 + checks/branch_protected.go | 1 + checks/branch_protected_test.go | 1 + checks/ci_tests.go | 1 + checks/code_review.go | 1 + checks/contributors.go | 1 + checks/frozen_deps.go | 1 + checks/fuzzing.go | 1 + checks/packaging.go | 1 + checks/permissions.go | 1 + checks/pull_requests.go | 1 + checks/sast.go | 1 + checks/shell_download_validate.go | 1 + checks/signed_releases.go | 1 + checks/signed_tags.go | 1 + checks/vulnerabilities.go | 1 + pkg/scorecard.go | 1 + pkg/scorecard_result.go | 1 + 19 files changed, 19 insertions(+) diff --git a/checks/active.go b/checks/active.go index 89f88b89329..3ccd71008fb 100644 --- a/checks/active.go +++ b/checks/active.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "time" diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 57dfb3575cf..cadd6d1cccc 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "fmt" "path/filepath" diff --git a/checks/branch_protected.go b/checks/branch_protected.go index 3129cfd140f..ab39b3ec105 100644 --- a/checks/branch_protected.go +++ b/checks/branch_protected.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "context" "errors" diff --git a/checks/branch_protected_test.go b/checks/branch_protected_test.go index 0285759daa8..b1c872970f5 100644 --- a/checks/branch_protected_test.go +++ b/checks/branch_protected_test.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "context" "fmt" diff --git a/checks/ci_tests.go b/checks/ci_tests.go index b96c6d9f55e..d095e07a741 100644 --- a/checks/ci_tests.go +++ b/checks/ci_tests.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "fmt" "strings" diff --git a/checks/code_review.go b/checks/code_review.go index cd7c145f593..c41747d06d5 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "errors" "fmt" diff --git a/checks/contributors.go b/checks/contributors.go index eb2c8812808..9999f9f4588 100644 --- a/checks/contributors.go +++ b/checks/contributors.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "strings" diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index b6614ec164b..7b66094a136 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "fmt" "regexp" diff --git a/checks/fuzzing.go b/checks/fuzzing.go index a77cb887a3d..b605765d4a6 100644 --- a/checks/fuzzing.go +++ b/checks/fuzzing.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "fmt" diff --git a/checks/packaging.go b/checks/packaging.go index db05a40e97e..5eb348073ad 100644 --- a/checks/packaging.go +++ b/checks/packaging.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "path/filepath" "regexp" diff --git a/checks/permissions.go b/checks/permissions.go index e8987362eae..b929f1f2025 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "errors" "fmt" diff --git a/checks/pull_requests.go b/checks/pull_requests.go index cdc77cf1fa3..37b73ab4c61 100644 --- a/checks/pull_requests.go +++ b/checks/pull_requests.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "strings" diff --git a/checks/sast.go b/checks/sast.go index 9ede7663930..6a2d6baeb29 100644 --- a/checks/sast.go +++ b/checks/sast.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "errors" diff --git a/checks/shell_download_validate.go b/checks/shell_download_validate.go index a14b8ab6b21..88c4e61fe5e 100644 --- a/checks/shell_download_validate.go +++ b/checks/shell_download_validate.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "bufio" "bytes" diff --git a/checks/signed_releases.go b/checks/signed_releases.go index f2bd4a31e76..d2f4b97bf2d 100644 --- a/checks/signed_releases.go +++ b/checks/signed_releases.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "errors" "strings" diff --git a/checks/signed_tags.go b/checks/signed_tags.go index 4dee6dd8eef..945c06a57ec 100644 --- a/checks/signed_tags.go +++ b/checks/signed_tags.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "errors" diff --git a/checks/vulnerabilities.go b/checks/vulnerabilities.go index 71cb48a8a80..4f82c27943c 100644 --- a/checks/vulnerabilities.go +++ b/checks/vulnerabilities.go @@ -14,6 +14,7 @@ package checks +//nolint:gci import ( "bytes" "encoding/json" diff --git a/pkg/scorecard.go b/pkg/scorecard.go index f2ef43f41d6..926ceb25a76 100644 --- a/pkg/scorecard.go +++ b/pkg/scorecard.go @@ -14,6 +14,7 @@ package pkg +//nolint:gci import ( "context" "fmt" diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index 5be53601a8d..399d9a98d6c 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -14,6 +14,7 @@ package pkg +//nolint:gci import ( "encoding/csv" "encoding/json" From 0b509447f54c98ce8995003a395ff66585483481 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 20:44:41 +0000 Subject: [PATCH 26/31] doc --- checker/check_result.go | 1 + checks/write.md | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/checker/check_result.go b/checker/check_result.go index 8f7d6e46b0f..72ede88f908 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -21,6 +21,7 @@ import ( scorecarderrors "github.com/ossf/scorecard/errors" ) +// UPGRADEv2: to remove. const ( MaxResultConfidence = 10 HalfResultConfidence = 5 diff --git a/checks/write.md b/checks/write.md index 47b3bff5a1a..955fdea0dfd 100644 --- a/checks/write.md +++ b/checks/write.md @@ -1,31 +1,31 @@ # How to write a check The steps to writting a check are as follow: -1. Create a file under checks, say `checks/mycheck.go` -2. Decide on a name, register the check: +1. Create a file under `checks/` folder, say `checks/mycheck.go` +2. Give the check a name and register the check: ``` // Note: do not export the name: start its name with a lower-case letter. -const checkMyChech string = "My-Check" +const checkMyCheckName string = "My-Check" func init() { - registerCheck(checkBinaryArtifacts, BinaryArtifacts) + registerCheck(checkMyCheckName, EntryPointMyCheck) } ``` 3. Log information that is benfical to the user using `checker.DetailLogger`: * Use `checker.DetailLogger.Warn()` to provide detail on low-score results. This is showed when the user supplies the `show-results` option. * Use `checker.DetailLogger.Info()` to provide detail on high-score results. This is showed when the user supplies the `show-results` option. - * Use `checker.DetailLogger.Debug()` to provide detail on in verbose mode: this is showed only when the user supplies the `--verbosity Debug` option. -4. If the checks fails to run in a way that is irrecoverable, use `checker.CreateRuntimeErrorResult()` function. An exmple of this is if an error is returned from an API you call. + * Use `checker.DetailLogger.Debug()` to provide detail in verbose mode: this is showed only when the user supplies the `--verbosity Debug` option. +4. If the checks fails in a way that is irrecoverable, return a result with `checker.CreateRuntimeErrorResult()` function: For example, +if an error is returned from an API you call, use the function. 5. Create the result of the check as follow: * Always provide a high-level sentence explaining the result/score of the check. - * If the check runs properly but is unable to conclude babout the score, use `checker.CreateInconclusiveResult()` function. + * If the check runs properly but is unable to determine a score, use `checker.CreateInconclusiveResult()` function. * For propertional results, use `checker.CreateProportionalScoreResult()`. - * For maximum score, use `checker.CreateMaxScoreResult()`; for min score use `checker.CreateMinScoreResult()` + * For maximum score, use `checker.CreateMaxScoreResult()`; for min score use `checker.CreateMinScoreResult()`. * If you need more flexibility and need to set a specific score, use `checker.CreateResultWithScore()` with one of the constants declared, such as `checker.HalfResultScore`. - -- 6. Dealing with errors: see [../errors/errors.md](errors/errors/md). -7. Create unit tests for both low, high and inconclusive score. Put them in a file `checks/mycheck_test.go` -8. Create e2e tests in `e2e/mycheck_test.go`. Use a dedicated repo whereata will not change over time, so that it's reliable for the tests. +7. Create unit tests for both low, high and inconclusive score. Put them in a file `checks/mycheck_test.go`. +8. Create e2e tests in `e2e/mycheck_test.go`. Use a dedicated repo that will not change over time, so that it's reliable for the tests. 9. Update the `checks/checks.yaml` with the description of your check. 10. Gerenate the `checks/check.md` using `go build && cd checks/main && ./main`. Verify `checks/check.md` was updated. 10. Update the [README.md](https://github.com/ossf/scorecard#scorecard-checks) with a short description of your check. From bd658a0856d0f2c72ba2b576038c5c19116c07c7 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 20:46:40 +0000 Subject: [PATCH 27/31] typo --- errors/errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors/errors.md b/errors/errors.md index c737ba3f62f..01ee98e99e8 100644 --- a/errors/errors.md +++ b/errors/errors.md @@ -13,7 +13,7 @@ import sce "github.com/ossf/scorecard/errors" // consistent erorr messages to the caller. return sce.Create(sce.ErrRunFailure, ErrInternalInvalidYamlFile.Error()) -// Return a standard check run failure, with an error message from an internal error and and API call error. +// Return a standard check run failure, with an error message from an internal error and an API call error. err := dependency.apiCall() if err != nil { return sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", sce.ErrInternalSomething, err)) From ee22c4a6f133d58e095da25bfb88d22c47411f81 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Fri, 16 Jul 2021 20:49:57 +0000 Subject: [PATCH 28/31] typo --- checks/checks2.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checks/checks2.yaml b/checks/checks2.yaml index 8b4726c62d8..a549b198f10 100644 --- a/checks/checks2.yaml +++ b/checks/checks2.yaml @@ -71,7 +71,7 @@ checks: - >- Enforce the rule for administrators / code owners as well. E.g. [GitHub](https://docs.github.com/en/github/administering-a-repository/about-protected-branches#include-administrators) - Frozen-Deps: + Frozen-Deps: risk: Medium description: >- This check tries to determine if a project has declared and pinned its From ac693699e54effb324b99774ded246e241f12db7 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Mon, 19 Jul 2021 18:35:16 +0000 Subject: [PATCH 29/31] test linter --- checks/binary_artifact.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index cadd6d1cccc..2f66ee4b222 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -14,13 +14,13 @@ package checks -//nolint:gci import ( "fmt" "path/filepath" "github.com/h2non/filetype" "github.com/h2non/filetype/types" + "github.com/ossf/scorecard/checker" sce "github.com/ossf/scorecard/errors" ) From cd1a68aca328d94e0398a8e30e38fa927f6d097b Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Mon, 19 Jul 2021 19:08:36 +0000 Subject: [PATCH 30/31] active check --- checker/check_result.go | 5 +++-- checks/active.go | 33 +++++++++++++++---------------- checks/branch_protected.go | 2 +- checks/branch_protected_test.go | 2 +- checks/checks2.yaml | 13 ++++++++++++ checks/ci_tests.go | 2 +- checks/code_review.go | 4 ++-- checks/contributors.go | 2 +- checks/frozen_deps.go | 4 ++-- checks/fuzzing.go | 2 +- checks/packaging.go | 2 +- checks/permissions.go | 4 ++-- checks/pull_requests.go | 2 +- checks/sast.go | 2 +- checks/shell_download_validate.go | 4 ++-- checks/signed_releases.go | 2 +- checks/signed_tags.go | 4 ++-- checks/vulnerabilities.go | 2 +- 18 files changed, 52 insertions(+), 39 deletions(-) diff --git a/checker/check_result.go b/checker/check_result.go index 72ede88f908..cd9123089aa 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -17,6 +17,7 @@ package checker import ( "errors" "fmt" + "math" scorecarderrors "github.com/ossf/scorecard/errors" ) @@ -92,7 +93,7 @@ func CreateResultWithScore(name, reason string, score int) CheckResult { func CreateProportionalScoreResult(name, reason string, b, t int) CheckResult { pass := true //nolint - score := 10 * b / t + score := int(math.Min(float64(10*b/t), float64(10))) //nolint if score < 8 { pass = false @@ -109,7 +110,7 @@ func CreateProportionalScoreResult(name, reason string, b, t int) CheckResult { //nolint Version: 2, Error2: nil, - Score2: 10 * b / t, + Score2: score, Reason2: fmt.Sprintf("%v -- score normalized to %d", reason, score), } } diff --git a/checks/active.go b/checks/active.go index 3ccd71008fb..3bc079b9c91 100644 --- a/checks/active.go +++ b/checks/active.go @@ -14,52 +14,51 @@ package checks -//nolint:gci import ( + "fmt" "time" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" ) const ( - // CheckActive is the registered name for IsActive. - CheckActive = "Active" - lookbackDays = 90 + checkActive = "Active" + lookBackMonths = 3 + commitsPerWeek = 1 ) //nolint:gochecknoinits func init() { - registerCheck(CheckActive, IsActive) + registerCheck(checkActive, IsActive) } func IsActive(c *checker.CheckRequest) checker.CheckResult { commits, _, err := c.Client.Repositories.ListCommits(c.Ctx, c.Owner, c.Repo, &github.CommitsListOptions{}) if err != nil { - return checker.MakeRetryResult(CheckActive, err) + return checker.CreateRuntimeErrorResult(checkActive, err) } tz, err := time.LoadLocation("UTC") if err != nil { - return checker.MakeRetryResult(CheckActive, err) + return checker.CreateRuntimeErrorResult(checkActive, sce.Create(sce.ErrRunFailure, fmt.Sprintf("time.LoadLocation: %v", err))) } - threshold := time.Now().In(tz).AddDate(0, 0, -1*lookbackDays) + threshold := time.Now().In(tz).AddDate(0, 0, -1*lookBackMonths*30) totalCommits := 0 for _, commit := range commits { commitFull, _, err := c.Client.Git.GetCommit(c.Ctx, c.Owner, c.Repo, commit.GetSHA()) if err != nil { - return checker.MakeRetryResult(CheckActive, err) + return checker.CreateRuntimeErrorResult(checkActive, err) } if commitFull.GetAuthor().GetDate().After(threshold) { totalCommits++ } } - c.Logf("commits in last %d days: %d", lookbackDays, totalCommits) - const numCommits = 2 - const confidence = 10 - return checker.CheckResult{ - Name: CheckActive, - Pass: totalCommits >= numCommits, - Confidence: confidence, - } + + return checker.CreateProportionalScoreResult(checkActive, + fmt.Sprintf("%d commit(s) found in the last %d days", totalCommits, lookBackMonths*30), + totalCommits, commitsPerWeek*lookBackMonths*4) + return checker.CreateMinScoreResult(checkActive, fmt.Sprintf("no commit found in the last %d days", lookBackMonths*30)) } diff --git a/checks/branch_protected.go b/checks/branch_protected.go index ab39b3ec105..286eb1b5371 100644 --- a/checks/branch_protected.go +++ b/checks/branch_protected.go @@ -14,13 +14,13 @@ package checks -//nolint:gci import ( "context" "errors" "regexp" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/branch_protected_test.go b/checks/branch_protected_test.go index b1c872970f5..106fdc229e4 100644 --- a/checks/branch_protected_test.go +++ b/checks/branch_protected_test.go @@ -14,7 +14,6 @@ package checks -//nolint:gci import ( "context" "fmt" @@ -22,6 +21,7 @@ import ( "testing" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/checks2.yaml b/checks/checks2.yaml index a549b198f10..485a3a0d0f4 100644 --- a/checks/checks2.yaml +++ b/checks/checks2.yaml @@ -15,6 +15,19 @@ # This is the source of truth for all check descriptions and remediation steps. # Run `cd checks/main && go run /main` to generate `checks.json` and `checks.md`. checks: + Active: + risk: High + description: >- + This check tries to determine if the project is "actively maintained". + + A project which is not active may not be patched, may not have its + dependencies patched, or may not be actively tested and used. So It + currently works by looking for commits within the last 90 days, and + outputs the highest score if there are at least 1 commit/week during this period. + remediation: + - >- + There is *NO* remediation work needed here. This is just to indicate + your project activity and maintenance commitment. Binary-Artifacts: risk: High description: >- diff --git a/checks/ci_tests.go b/checks/ci_tests.go index d095e07a741..87073b290c2 100644 --- a/checks/ci_tests.go +++ b/checks/ci_tests.go @@ -14,12 +14,12 @@ package checks -//nolint:gci import ( "fmt" "strings" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/code_review.go b/checks/code_review.go index c41747d06d5..9a9e5abacdb 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -14,15 +14,15 @@ package checks -//nolint:gci import ( "errors" "fmt" "strings" "github.com/google/go-github/v32/github" - "github.com/ossf/scorecard/checker" "github.com/shurcooL/githubv4" + + "github.com/ossf/scorecard/checker" ) const ( diff --git a/checks/contributors.go b/checks/contributors.go index 9999f9f4588..7a4d1b32d00 100644 --- a/checks/contributors.go +++ b/checks/contributors.go @@ -14,11 +14,11 @@ package checks -//nolint:gci import ( "strings" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index 7b66094a136..85b43f026bd 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -14,16 +14,16 @@ package checks -//nolint:gci import ( "fmt" "regexp" "strings" "github.com/moby/buildkit/frontend/dockerfile/parser" + "gopkg.in/yaml.v2" + "github.com/ossf/scorecard/checker" sce "github.com/ossf/scorecard/errors" - "gopkg.in/yaml.v2" ) // checkFrozenDeps is the registered name for FrozenDeps. diff --git a/checks/fuzzing.go b/checks/fuzzing.go index b605765d4a6..84ac24e6495 100644 --- a/checks/fuzzing.go +++ b/checks/fuzzing.go @@ -14,11 +14,11 @@ package checks -//nolint:gci import ( "fmt" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/packaging.go b/checks/packaging.go index 5eb348073ad..4bbcad7052a 100644 --- a/checks/packaging.go +++ b/checks/packaging.go @@ -14,13 +14,13 @@ package checks -//nolint:gci import ( "path/filepath" "regexp" "strings" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/permissions.go b/checks/permissions.go index b929f1f2025..5e2bc1dae13 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -14,15 +14,15 @@ package checks -//nolint:gci import ( "errors" "fmt" "strings" + "gopkg.in/yaml.v2" + "github.com/ossf/scorecard/checker" sce "github.com/ossf/scorecard/errors" - "gopkg.in/yaml.v2" ) const CheckPermissions = "Token-Permissions" diff --git a/checks/pull_requests.go b/checks/pull_requests.go index 37b73ab4c61..5f9fc1f96b2 100644 --- a/checks/pull_requests.go +++ b/checks/pull_requests.go @@ -14,11 +14,11 @@ package checks -//nolint:gci import ( "strings" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/sast.go b/checks/sast.go index 6a2d6baeb29..72b22185004 100644 --- a/checks/sast.go +++ b/checks/sast.go @@ -14,11 +14,11 @@ package checks -//nolint:gci import ( "errors" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/shell_download_validate.go b/checks/shell_download_validate.go index 88c4e61fe5e..6c87209e4cc 100644 --- a/checks/shell_download_validate.go +++ b/checks/shell_download_validate.go @@ -14,7 +14,6 @@ package checks -//nolint:gci import ( "bufio" "bytes" @@ -25,9 +24,10 @@ import ( "regexp" "strings" + "mvdan.cc/sh/v3/syntax" + "github.com/ossf/scorecard/checker" sce "github.com/ossf/scorecard/errors" - "mvdan.cc/sh/v3/syntax" ) // List of interpreters. diff --git a/checks/signed_releases.go b/checks/signed_releases.go index d2f4b97bf2d..6d4873697e9 100644 --- a/checks/signed_releases.go +++ b/checks/signed_releases.go @@ -14,12 +14,12 @@ package checks -//nolint:gci import ( "errors" "strings" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) diff --git a/checks/signed_tags.go b/checks/signed_tags.go index 945c06a57ec..85bcd6428c7 100644 --- a/checks/signed_tags.go +++ b/checks/signed_tags.go @@ -14,12 +14,12 @@ package checks -//nolint:gci import ( "errors" - "github.com/ossf/scorecard/checker" "github.com/shurcooL/githubv4" + + "github.com/ossf/scorecard/checker" ) const ( diff --git a/checks/vulnerabilities.go b/checks/vulnerabilities.go index 4f82c27943c..a9453992ae9 100644 --- a/checks/vulnerabilities.go +++ b/checks/vulnerabilities.go @@ -14,7 +14,6 @@ package checks -//nolint:gci import ( "bytes" "encoding/json" @@ -23,6 +22,7 @@ import ( "strings" "github.com/google/go-github/v32/github" + "github.com/ossf/scorecard/checker" ) From c4a99becb6f34a5287d2f648238db1cb142f9ad9 Mon Sep 17 00:00:00 2001 From: laurentsimon Date: Mon, 19 Jul 2021 23:43:52 +0000 Subject: [PATCH 31/31] active and bp checks --- checker/check_result.go | 1 - checks/active.go | 23 +- checks/automatic_dependency_update.go | 12 +- checks/binary_artifact.go | 13 +- checks/branch_protected.go | 107 ++-- checks/branch_protected_test.go | 810 ++++++++++++-------------- checks/checks2.yaml | 28 +- checks/code_review.go | 18 +- checks/frozen_deps.go | 44 +- checks/frozen_deps_test.go | 277 ++++----- checks/permissions_test.go | 9 +- e2e/active_test.go | 22 +- e2e/automatic_deps_test.go | 5 +- e2e/binary_artifacts_test.go | 4 +- e2e/branchprotection_test.go | 24 +- e2e/code_review_test.go | 2 +- e2e/frozen_deps_test.go | 4 +- errors/internal.go | 2 + pkg/scorecard_result.go | 2 +- utests/utlib.go | 48 +- 20 files changed, 706 insertions(+), 749 deletions(-) diff --git a/checker/check_result.go b/checker/check_result.go index cd9123089aa..0c14e05955b 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -173,7 +173,6 @@ func MakeAndResult2(checks ...CheckResult) CheckResult { } worseResult := checks[0] - // UPGRADEv2: will go away after old struct is removed. //nolint for _, result := range checks[1:] { diff --git a/checks/active.go b/checks/active.go index 3bc079b9c91..8a36f627866 100644 --- a/checks/active.go +++ b/checks/active.go @@ -25,40 +25,41 @@ import ( ) const ( - checkActive = "Active" - lookBackMonths = 3 + CheckActive = "Active" + lookBackDays = 90 commitsPerWeek = 1 + daysInOneWeek = 7 ) //nolint:gochecknoinits func init() { - registerCheck(checkActive, IsActive) + registerCheck(CheckActive, IsActive) } func IsActive(c *checker.CheckRequest) checker.CheckResult { commits, _, err := c.Client.Repositories.ListCommits(c.Ctx, c.Owner, c.Repo, &github.CommitsListOptions{}) if err != nil { - return checker.CreateRuntimeErrorResult(checkActive, err) + return checker.CreateRuntimeErrorResult(CheckActive, err) } tz, err := time.LoadLocation("UTC") if err != nil { - return checker.CreateRuntimeErrorResult(checkActive, sce.Create(sce.ErrRunFailure, fmt.Sprintf("time.LoadLocation: %v", err))) + return checker.CreateRuntimeErrorResult(CheckActive, sce.Create(sce.ErrRunFailure, fmt.Sprintf("time.LoadLocation: %v", err))) } - threshold := time.Now().In(tz).AddDate(0, 0, -1*lookBackMonths*30) + threshold := time.Now().In(tz).AddDate(0, 0, -1*lookBackDays) totalCommits := 0 for _, commit := range commits { commitFull, _, err := c.Client.Git.GetCommit(c.Ctx, c.Owner, c.Repo, commit.GetSHA()) if err != nil { - return checker.CreateRuntimeErrorResult(checkActive, err) + return checker.CreateRuntimeErrorResult(CheckActive, err) } if commitFull.GetAuthor().GetDate().After(threshold) { totalCommits++ } } - return checker.CreateProportionalScoreResult(checkActive, - fmt.Sprintf("%d commit(s) found in the last %d days", totalCommits, lookBackMonths*30), - totalCommits, commitsPerWeek*lookBackMonths*4) - return checker.CreateMinScoreResult(checkActive, fmt.Sprintf("no commit found in the last %d days", lookBackMonths*30)) + return checker.CreateProportionalScoreResult(CheckActive, + fmt.Sprintf("%d commit(s) found in the last %d days", totalCommits, lookBackDays), + totalCommits, commitsPerWeek*lookBackDays/daysInOneWeek) + return checker.CreateMinScoreResult(CheckActive, fmt.Sprintf("no commit found in the last %d days", lookBackDays)) } diff --git a/checks/automatic_dependency_update.go b/checks/automatic_dependency_update.go index 6957b8ff53c..2ecab308ff2 100644 --- a/checks/automatic_dependency_update.go +++ b/checks/automatic_dependency_update.go @@ -20,25 +20,25 @@ import ( "github.com/ossf/scorecard/checker" ) -const checkAutomaticDependencyUpdate = "Automatic-Dependency-Update" +const CheckAutomaticDependencyUpdate = "Automatic-Dependency-Update" //nolint func init() { - registerCheck(checkAutomaticDependencyUpdate, AutomaticDependencyUpdate) + registerCheck(CheckAutomaticDependencyUpdate, AutomaticDependencyUpdate) } // AutomaticDependencyUpdate will check the repository if it contains Automatic dependency update. func AutomaticDependencyUpdate(c *checker.CheckRequest) checker.CheckResult { - r, err := CheckIfFileExists2(checkAutomaticDependencyUpdate, c, fileExists) + r, err := CheckIfFileExists2(CheckAutomaticDependencyUpdate, c, fileExists) if err != nil { - return checker.CreateRuntimeErrorResult(checkAutomaticDependencyUpdate, err) + return checker.CreateRuntimeErrorResult(CheckAutomaticDependencyUpdate, err) } if !r { - return checker.CreateMinScoreResult(checkAutomaticDependencyUpdate, "no tool detected [dependabot|renovabot]") + return checker.CreateMinScoreResult(CheckAutomaticDependencyUpdate, "no tool detected [dependabot|renovabot]") } // High score result. - return checker.CreateMaxScoreResult(checkAutomaticDependencyUpdate, "tool detected") + return checker.CreateMaxScoreResult(CheckAutomaticDependencyUpdate, "tool detected") } // fileExists will validate the if frozen dependencies file name exists. diff --git a/checks/binary_artifact.go b/checks/binary_artifact.go index 2f66ee4b222..f5e0e0e72f5 100644 --- a/checks/binary_artifact.go +++ b/checks/binary_artifact.go @@ -25,25 +25,24 @@ import ( sce "github.com/ossf/scorecard/errors" ) +const CheckBinaryArtifacts string = "Binary-Artifacts" + //nolint func init() { - registerCheck(checkBinaryArtifacts, BinaryArtifacts) + registerCheck(CheckBinaryArtifacts, BinaryArtifacts) } -// TODO: read the check code from file? -const checkBinaryArtifacts string = "Binary-Artifacts" - // BinaryArtifacts will check the repository if it contains binary artifacts. func BinaryArtifacts(c *checker.CheckRequest) checker.CheckResult { r, err := CheckFilesContent2("*", false, c, checkBinaryFileContent) if err != nil { - return checker.CreateRuntimeErrorResult(checkBinaryArtifacts, err) + return checker.CreateRuntimeErrorResult(CheckBinaryArtifacts, err) } if !r { - return checker.CreateMinScoreResult(checkBinaryArtifacts, "binaries present in source code") + return checker.CreateMinScoreResult(CheckBinaryArtifacts, "binaries present in source code") } - return checker.CreateMaxScoreResult(checkBinaryArtifacts, "no binaries found in the repo") + return checker.CreateMaxScoreResult(CheckBinaryArtifacts, "no binaries found in the repo") } func checkBinaryFileContent(path string, content []byte, diff --git a/checks/branch_protected.go b/checks/branch_protected.go index 286eb1b5371..0f6e40d1665 100644 --- a/checks/branch_protected.go +++ b/checks/branch_protected.go @@ -16,18 +16,18 @@ package checks import ( "context" - "errors" + "fmt" "regexp" "github.com/google/go-github/v32/github" "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" ) const ( - // CheckBranchProtection is the registered name for BranchProtection. CheckBranchProtection = "Branch-Protection" - minReviews = 1 + minReviews = 2 ) //nolint:gochecknoinits @@ -46,31 +46,23 @@ type repositories interface { *github.Protection, *github.Response, error) } -type logger func(s string, f ...interface{}) - -// ErrCommitishNil TargetCommitish nil for release. -var ErrCommitishNil = errors.New("target_commitish is nil for release") - -// ErrBranchNotFound branch from TargetCommitish not found. -var ErrBranchNotFound = errors.New("branch not found") - func BranchProtection(c *checker.CheckRequest) checker.CheckResult { - // Checks branch protection on both release and development branch - return checkReleaseAndDevBranchProtection(c.Ctx, c.Client.Repositories, c.Logf, c.Owner, c.Repo) + // Checks branch protection on both release and development branch. + return checkReleaseAndDevBranchProtection(c.Ctx, c.Client.Repositories, c.Dlogger, c.Owner, c.Repo) } -func checkReleaseAndDevBranchProtection(ctx context.Context, r repositories, l logger, ownerStr, +func checkReleaseAndDevBranchProtection(ctx context.Context, r repositories, dl checker.DetailLogger, ownerStr, repoStr string) checker.CheckResult { // Get all branches. This will include information on whether they are protected. branches, _, err := r.ListBranches(ctx, ownerStr, repoStr, &github.BranchListOptions{}) if err != nil { - return checker.MakeRetryResult(CheckBranchProtection, err) + return checker.CreateRuntimeErrorResult(CheckBranchProtection, err) } // Get release branches releases, _, err := r.ListReleases(ctx, ownerStr, repoStr, &github.ListOptions{}) if err != nil { - return checker.MakeRetryResult(CheckBranchProtection, err) + return checker.CreateRuntimeErrorResult(CheckBranchProtection, err) } var checks []checker.CheckResult @@ -79,7 +71,9 @@ func checkReleaseAndDevBranchProtection(ctx context.Context, r repositories, l l for _, release := range releases { if release.TargetCommitish == nil { // Log with a named error if target_commitish is nil. - checks = append(checks, checker.MakeFailResult(CheckBranchProtection, ErrCommitishNil)) + r := checker.CreateRuntimeErrorResult(CheckBranchProtection, + sce.Create(sce.ErrRunFailure, sce.ErrCommitishNil.Error())) + checks = append(checks, r) continue } @@ -92,7 +86,9 @@ func checkReleaseAndDevBranchProtection(ctx context.Context, r repositories, l l name, err := resolveBranchName(branches, *release.TargetCommitish) if err != nil { // If the commitish branch is still not found, fail. - checks = append(checks, checker.MakeFailResult(CheckBranchProtection, ErrBranchNotFound)) + r := checker.CreateRuntimeErrorResult(CheckBranchProtection, + sce.Create(sce.ErrRunFailure, sce.ErrBranchNotFound.Error())) + checks = append(checks, r) continue } @@ -100,10 +96,10 @@ func checkReleaseAndDevBranchProtection(ctx context.Context, r repositories, l l checkBranches[*name] = true } - // Add default branch + // Add default branch. repo, _, err := r.Get(ctx, ownerStr, repoStr) if err != nil { - return checker.MakeRetryResult(CheckBranchProtection, err) + return checker.CreateRuntimeErrorResult(CheckBranchProtection, err) } checkBranches[*repo.DefaultBranch] = true @@ -111,23 +107,20 @@ func checkReleaseAndDevBranchProtection(ctx context.Context, r repositories, l l for b := range checkBranches { protected, err := isBranchProtected(branches, b) if err != nil { - checks = append(checks, checker.MakeFailResult(CheckBranchProtection, ErrBranchNotFound)) + r := checker.CreateRuntimeErrorResult(CheckBranchProtection, sce.Create(sce.ErrRunFailure, sce.ErrBranchNotFound.Error())) + checks = append(checks, r) } if !protected { - l("!! branch protection not enabled for branch %s", b) - checks = append(checks, checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Confidence: checker.MaxResultConfidence, - }) + r := checker.CreateMinScoreResult(CheckBranchProtection, fmt.Sprintf("branch protection not enabled for branch '%s'", b)) + checks = append(checks, r) } else { // The branch is protected. Check the protection. - res := getProtectionAndCheck(ctx, r, l, ownerStr, repoStr, b) + res := getProtectionAndCheck(ctx, r, dl, ownerStr, repoStr, b) checks = append(checks, res) } } - return checker.MakeAndResult(checks...) + return checker.MakeAndResult2(checks...) } func resolveBranchName(branches []*github.Branch, name string) (*string, error) { @@ -144,7 +137,7 @@ func resolveBranchName(branches []*github.Branch, name string) (*string, error) return resolveBranchName(branches, "main") } - return nil, ErrBranchNotFound + return nil, sce.Create(sce.ErrRunFailure, sce.ErrBranchNotFound.Error()) } func isBranchProtected(branches []*github.Branch, name string) (bool, error) { @@ -154,71 +147,78 @@ func isBranchProtected(branches []*github.Branch, name string) (bool, error) { return b.GetProtected(), nil } } - return false, ErrBranchNotFound + + return false, sce.Create(sce.ErrRunFailure, sce.ErrBranchNotFound.Error()) } -func getProtectionAndCheck(ctx context.Context, r repositories, l logger, ownerStr, repoStr, +func getProtectionAndCheck(ctx context.Context, r repositories, dl checker.DetailLogger, ownerStr, repoStr, branch string) checker.CheckResult { // We only call this if the branch is protected. An error indicates not found. protection, resp, err := r.GetBranchProtection(ctx, ownerStr, repoStr, branch) const fileNotFound = 404 if resp.StatusCode == fileNotFound { - return checker.MakeRetryResult(CheckBranchProtection, err) + return checker.CreateRuntimeErrorResult(CheckBranchProtection, sce.Create(sce.ErrRunFailure, err.Error())) } - return IsBranchProtected(protection, branch, l) + return IsBranchProtected(protection, branch, dl) } -func IsBranchProtected(protection *github.Protection, branch string, l logger) checker.CheckResult { - totalChecks := 6 +func IsBranchProtected(protection *github.Protection, branch string, dl checker.DetailLogger) checker.CheckResult { + totalChecks := 10 totalSuccess := 0 // This is disabled by default (good). if protection.GetAllowForcePushes() != nil && protection.AllowForcePushes.Enabled { - l("!! branch protection - AllowForcePushes should be disabled on %s", branch) + dl.Warn("AllowForcePushes enabled on branch '%s'", branch) } else { + dl.Info("AllowForcePushes disabled on branch '%s'", branch) totalSuccess++ } // This is disabled by default (good). if protection.GetAllowDeletions() != nil && protection.AllowDeletions.Enabled { - l("!! branch protection - AllowDeletions should be disabled on %s", branch) + dl.Warn("AllowDeletions enabled on branch '%s'", branch) } else { + dl.Info("AllowDeletions disabled on branch '%s'", branch) totalSuccess++ } // This is disabled by default (bad). if protection.GetEnforceAdmins() != nil && protection.EnforceAdmins.Enabled { + dl.Info("EnforceAdmins disabled on branch '%s'", branch) totalSuccess++ } else { - l("!! branch protection - EnforceAdmins should be enabled on %s", branch) + dl.Warn("EnforceAdmins disabled on branch '%s'", branch) } // This is disabled by default (bad). if protection.GetRequireLinearHistory() != nil && protection.RequireLinearHistory.Enabled { + dl.Info("Linear history enabled on branch '%s'", branch) totalSuccess++ } else { - l("!! branch protection - Linear history should be enabled on %s", branch) + dl.Warn("Linear history disabled on branch '%s'", branch) } - if requiresStatusChecks(protection, branch, l) { + if requiresStatusChecks(protection, branch, dl) { + dl.Info("Strict status check enabled on branch '%s'", branch) totalSuccess++ } - if requiresThoroughReviews(protection, branch, l) { + if requiresThoroughReviews(protection, branch, dl) { totalSuccess++ } - return checker.MakeProportionalResult(CheckBranchProtection, totalSuccess, totalChecks, 1.0) + return checker.CreateProportionalScoreResult(CheckBranchProtection, + "%d out of %d branch protection settings are enabled", totalSuccess, totalChecks) } // Returns true if several PR status checks requirements are enabled. Otherwise returns false and logs why it failed. -func requiresStatusChecks(protection *github.Protection, branch string, l logger) bool { +func requiresStatusChecks(protection *github.Protection, branch string, dl checker.DetailLogger) bool { // This is disabled by default (bad). if protection.GetRequiredStatusChecks() != nil && protection.RequiredStatusChecks.Strict && @@ -228,17 +228,17 @@ func requiresStatusChecks(protection *github.Protection, branch string, l logger switch { case protection.RequiredStatusChecks == nil || !protection.RequiredStatusChecks.Strict: - l("!! branch protection - Status checks for merging should be enabled on %s", branch) + dl.Warn("Status checks for merging disabled on branch '%s'", branch) case len(protection.RequiredStatusChecks.Contexts) == 0: - l("!! branch protection - Status checks for merging should have specific status to check for on %s", branch) + dl.Warn("Status checks for merging have no specific status to check on branch '%s'", branch) default: - panic("!! branch protection - Unhandled status checks error") + panic("Unhandled status checks error") } return false } // Returns true if several PR review requirements are enabled. Otherwise returns false and logs why it failed. -func requiresThoroughReviews(protection *github.Protection, branch string, l logger) bool { +func requiresThoroughReviews(protection *github.Protection, branch string, dl checker.DetailLogger) bool { // This is disabled by default (bad). if protection.GetRequiredPullRequestReviews() != nil && protection.RequiredPullRequestReviews.RequiredApprovingReviewCount >= minReviews && @@ -248,18 +248,19 @@ func requiresThoroughReviews(protection *github.Protection, branch string, l log } switch { case protection.RequiredPullRequestReviews == nil: - l("!! branch protection - Pullrequest reviews should be enabled on %s", branch) + dl.Warn("Pullrequest reviews disabled on branch '%s'", branch) fallthrough case protection.RequiredPullRequestReviews.RequiredApprovingReviewCount < minReviews: - l("!! branch protection - %v pullrequest reviews should be enabled on %s", minReviews, branch) + dl.Warn("Number of required reviewers is only %d on branch '%s'", + protection.RequiredPullRequestReviews.RequiredApprovingReviewCount, branch) fallthrough case !protection.RequiredPullRequestReviews.DismissStaleReviews: - l("!! branch protection - Stale review dismissal should be enabled on %s", branch) + dl.Warn("Stale review dismissal disabled on branch '%s'", branch) fallthrough case !protection.RequiredPullRequestReviews.RequireCodeOwnerReviews: - l("!! branch protection - Owner review should be enabled on %s", branch) + dl.Warn("Owner review not required on branch '%s'", branch) default: - panic("!! branch protection - Unhandled pull request error") + panic("Unhandled pull request error") } return false } diff --git a/checks/branch_protected_test.go b/checks/branch_protected_test.go index 106fdc229e4..b2e8e5eacc2 100644 --- a/checks/branch_protected_test.go +++ b/checks/branch_protected_test.go @@ -16,24 +16,16 @@ package checks import ( "context" - "fmt" "net/http" "testing" "github.com/google/go-github/v32/github" "github.com/ossf/scorecard/checker" + sce "github.com/ossf/scorecard/errors" + scut "github.com/ossf/scorecard/utests" ) -// TODO: these logging functions are repeated from lib/check_fn.go. Reuse code. -type log struct { - messages []string -} - -func (l *log) Logf(s string, f ...interface{}) { - l.messages = append(l.messages, fmt.Sprintf(s, f...)) -} - type mockRepos struct { branches []*string protections map[string]*github.Protection @@ -68,7 +60,8 @@ func (m mockRepos) GetBranchProtection(ctx context.Context, o string, r string, return nil, &github.Response{ Response: &http.Response{StatusCode: http.StatusNotFound}, }, - ErrBranchNotFound + //nolint + sce.Create(sce.ErrRunFailure, sce.ErrBranchNotFound.Error()) } func (m mockRepos) ListBranches(ctx context.Context, owner string, repo string, @@ -81,9 +74,8 @@ func (m mockRepos) ListBranches(ctx context.Context, owner string, repo string, return res, nil, nil } -func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mocks return different results per test case +func TestReleaseAndDevBranchProtected(t *testing.T) { t.Parallel() - l := log{} rel1 := "release/v.1" sha := "8fb3cb86082b17144a80402f5367ae65f06083bd" @@ -91,14 +83,21 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock //nolint tests := []struct { name string + expected scut.TestReturn branches []*string defaultBranch *string releases []*string protections map[string]*github.Protection - want checker.CheckResult }{ { - name: "Only development branch", + name: "Only development branch", + expected: scut.TestReturn{ + Errors: nil, + Score: 2, + NumberOfWarn: 6, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, defaultBranch: &main, branches: []*string{&rel1, &main}, releases: nil, @@ -137,17 +136,16 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock }, }, }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 7, - ShouldRetry: false, - Error: nil, - }, }, { - name: "Take worst of release and development", + name: "Take worst of release and development", + expected: scut.TestReturn{ + Errors: nil, + Score: 2, + NumberOfWarn: 9, + NumberOfInfo: 7, + NumberOfDebug: 0, + }, defaultBranch: &main, branches: []*string{&rel1, &main}, releases: []*string{&rel1}, @@ -219,17 +217,16 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock }, }, }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 7, - ShouldRetry: false, - Error: nil, - }, }, { - name: "Both release and development are OK", + name: "Both release and development are OK", + expected: scut.TestReturn{ + Errors: nil, + Score: 5, + NumberOfWarn: 6, + NumberOfInfo: 10, + NumberOfDebug: 0, + }, defaultBranch: &main, branches: []*string{&rel1, &main}, releases: []*string{&rel1}, @@ -301,17 +298,16 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock }, }, }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: true, - Details: nil, - Confidence: 10, - ShouldRetry: false, - Error: nil, - }, }, { - name: "Ignore a non-branch targetcommitish", + name: "Ignore a non-branch targetcommitish", + expected: scut.TestReturn{ + Errors: nil, + Score: 2, + NumberOfWarn: 6, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, defaultBranch: &main, branches: []*string{&rel1, &main}, releases: []*string{&sha}, @@ -350,17 +346,16 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock }, }, }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 7, - ShouldRetry: false, - Error: nil, - }, }, { - name: "TargetCommittish nil", + name: "TargetCommittish nil", + expected: scut.TestReturn{ + Errors: []error{sce.ErrRunFailure}, + Score: checker.InconclusiveResultScore, + NumberOfWarn: 6, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, defaultBranch: &main, branches: []*string{&main}, releases: []*string{nil}, @@ -399,476 +394,443 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock }, }, }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 10, - ShouldRetry: false, - Error: ErrCommitishNil, - }, }, } - for _, tt := range tests { //nolint:paralleltest // mocks return different results per test case + for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below - l.messages = []string{} - t.Run(tt.name, func(t *testing.T) { + t.Parallel() m := mockRepos{ defaultBranch: tt.defaultBranch, branches: tt.branches, releases: tt.releases, protections: tt.protections, } - got := checkReleaseAndDevBranchProtection(context.Background(), m, - l.Logf, "testowner", "testrepo") - got.Details = l.messages - if got.Confidence != tt.want.Confidence || got.Pass != tt.want.Pass { - t.Errorf("IsBranchProtected() = %s, %v, want %v", tt.name, got, tt.want) - } + dl := scut.TestDetailLogger{} + r := checkReleaseAndDevBranchProtection(context.Background(), m, + &dl, "testowner", "testrepo") + scut.ValidateTestReturn2(t, tt.name, &tt.expected, &r, &dl) }) } } func TestIsBranchProtected(t *testing.T) { t.Parallel() - type args struct { - protection *github.Protection - } - l := log{} tests := []struct { - name string - args args - want checker.CheckResult + name string + protection *github.Protection + expected scut.TestReturn }{ { name: "Nothing is enabled", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: false, - Contexts: nil, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 0, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: false, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 5, + NumberOfWarn: 3, + NumberOfInfo: 5, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: false, + Contexts: nil, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: false, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 0, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: false, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: false, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 7, - ShouldRetry: false, - Error: nil, }, }, { name: "Required status check enabled", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"foo"}, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 0, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: false, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 3, + NumberOfWarn: 5, + NumberOfInfo: 3, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: true, + Contexts: []string{"foo"}, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: false, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 0, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: false, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: false, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 5, - ShouldRetry: false, - Error: nil, }, }, { name: "Required status check enabled without checking for status string", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: true, - Contexts: nil, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 0, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: false, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 2, + NumberOfWarn: 6, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: true, + Contexts: nil, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: false, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 0, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: false, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: false, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 7, - ShouldRetry: false, - Error: nil, }, }, - { name: "Required pull request enabled", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"foo"}, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 1, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: false, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 3, + NumberOfWarn: 5, + NumberOfInfo: 3, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: false, + Contexts: []string{"foo"}, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: true, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 1, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: false, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: true, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 5, - ShouldRetry: false, - Error: nil, }, }, { name: "Required admin enforcement enabled", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"foo"}, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 0, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: true, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 3, + NumberOfWarn: 5, + NumberOfInfo: 3, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: false, + Contexts: []string{"foo"}, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: false, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 0, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: true, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: false, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 5, - ShouldRetry: false, - Error: nil, }, }, { name: "Required linear history enabled", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"foo"}, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 0, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: false, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 3, + NumberOfWarn: 5, + NumberOfInfo: 3, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: false, + Contexts: []string{"foo"}, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: true, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 0, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: false, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: true, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 5, - ShouldRetry: false, - Error: nil, }, }, { name: "Allow force push enabled", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"foo"}, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 0, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: false, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 1, + NumberOfWarn: 7, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: false, + Contexts: []string{"foo"}, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: false, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: true, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 0, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: false, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: false, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: true, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 9, - ShouldRetry: false, - Error: nil, }, }, { name: "Allow deletions enabled", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"foo"}, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: false, - RequireCodeOwnerReviews: false, - RequiredApprovingReviewCount: 0, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: false, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 1, + NumberOfWarn: 7, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: false, + Contexts: []string{"foo"}, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: false, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: true, }, + DismissStaleReviews: false, + RequireCodeOwnerReviews: false, + RequiredApprovingReviewCount: 0, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: false, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: false, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: true, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: false, - Details: nil, - Confidence: 9, - ShouldRetry: false, - Error: nil, }, }, { name: "Branches are protected", - args: args{ - protection: &github.Protection{ - RequiredStatusChecks: &github.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"foo"}, - }, - RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ - DismissalRestrictions: &github.DismissalRestrictions{ - Users: nil, - Teams: nil, - }, - DismissStaleReviews: true, - RequireCodeOwnerReviews: true, - RequiredApprovingReviewCount: 1, - }, - EnforceAdmins: &github.AdminEnforcement{ - URL: nil, - Enabled: true, - }, - Restrictions: &github.BranchRestrictions{ + expected: scut.TestReturn{ + Errors: nil, + Score: 1, + NumberOfWarn: 7, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, + protection: &github.Protection{ + RequiredStatusChecks: &github.RequiredStatusChecks{ + Strict: true, + Contexts: []string{"foo"}, + }, + RequiredPullRequestReviews: &github.PullRequestReviewsEnforcement{ + DismissalRestrictions: &github.DismissalRestrictions{ Users: nil, Teams: nil, - Apps: nil, - }, - RequireLinearHistory: &github.RequireLinearHistory{ - Enabled: true, - }, - AllowForcePushes: &github.AllowForcePushes{ - Enabled: false, - }, - AllowDeletions: &github.AllowDeletions{ - Enabled: false, }, + DismissStaleReviews: true, + RequireCodeOwnerReviews: true, + RequiredApprovingReviewCount: 1, + }, + EnforceAdmins: &github.AdminEnforcement{ + URL: nil, + Enabled: true, + }, + Restrictions: &github.BranchRestrictions{ + Users: nil, + Teams: nil, + Apps: nil, + }, + RequireLinearHistory: &github.RequireLinearHistory{ + Enabled: true, + }, + AllowForcePushes: &github.AllowForcePushes{ + Enabled: false, + }, + AllowDeletions: &github.AllowDeletions{ + Enabled: false, }, - }, - want: checker.CheckResult{ - Name: CheckBranchProtection, - Pass: true, - Details: nil, - Confidence: 10, - ShouldRetry: false, - Error: nil, }, }, } + // for _, tt := range tests { + // tt := tt // Re-initializing variable so it is not changed while executing the closure below + // l.messages = []string{} + // t.Run(tt.name, func(t *testing.T) { + // t.Parallel() + // got := IsBranchProtected(tt.args.protection, "test", l.Logf) + // got.Details = l.messages + // if got.Confidence != tt.want.Confidence || got.Pass != tt.want.Pass { + // t.Errorf("IsBranchProtected() = %s, %v, want %v", tt.name, got, tt.want) + // } + // }) + // } + for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below - l.messages = []string{} t.Run(tt.name, func(t *testing.T) { t.Parallel() - got := IsBranchProtected(tt.args.protection, "test", l.Logf) - got.Details = l.messages - if got.Confidence != tt.want.Confidence || got.Pass != tt.want.Pass { - t.Errorf("IsBranchProtected() = %s, %v, want %v", tt.name, got, tt.want) - } + dl := scut.TestDetailLogger{} + r := IsBranchProtected(tt.protection, "test", &dl) + scut.ValidateTestReturn2(t, tt.name, &tt.expected, &r, &dl) }) } } diff --git a/checks/checks2.yaml b/checks/checks2.yaml index 485a3a0d0f4..87080a4c148 100644 --- a/checks/checks2.yaml +++ b/checks/checks2.yaml @@ -21,8 +21,10 @@ checks: This check tries to determine if the project is "actively maintained". A project which is not active may not be patched, may not have its - dependencies patched, or may not be actively tested and used. So It - currently works by looking for commits within the last 90 days, and + dependencies patched, or may not be actively tested and used. + A low score is therefore considered `High` risk. + + The check currently works by looking for commits within the last 90 days, and outputs the highest score if there are at least 1 commit/week during this period. remediation: - >- @@ -84,6 +86,28 @@ checks: - >- Enforce the rule for administrators / code owners as well. E.g. [GitHub](https://docs.github.com/en/github/administering-a-repository/about-protected-branches#include-administrators) + Branch-Protection: + risk: High + description: >- + Branch protection allows defining rules to enforce certain workflows for + branches, such as requiring a review or passing certain status checks. + + Branch protection ensures compromised contributors cannot + intentionally inject malicious code. A low score is therefore considered `High` risk. + + This check determines if the default and release branches are + protected with GitHub's branch protection settings. + The check only works when the token has [Admin + access](https://github.community/t/enable-branch-protection-get-api-without-admin/14197) + to the repository. This check determines if the default and release branches are + protected. + remediation: + - >- + Enable branch protection settings in your source hosting provider to + avoid force pushes or deletion of your important branches. + - >- + For GitHub, check out the steps + [here](https://docs.github.com/en/github/administering-a-repository/managing-a-branch-protection-rule). Frozen-Deps: risk: Medium description: >- diff --git a/checks/code_review.go b/checks/code_review.go index 9a9e5abacdb..14b714454f4 100644 --- a/checks/code_review.go +++ b/checks/code_review.go @@ -26,8 +26,8 @@ import ( ) const ( - // checkCodeReview is the registered name for DoesCodeReview. - checkCodeReview = "Code-Review" + // CheckCodeReview is the registered name for DoesCodeReview. + CheckCodeReview = "Code-Review" pullRequestsToAnalyze = 30 reviewsToAnalyze = 30 labelsToAnalyze = 30 @@ -71,7 +71,7 @@ var ( //nolint:gochecknoinits func init() { - registerCheck(checkCodeReview, DoesCodeReview) + registerCheck(CheckCodeReview, DoesCodeReview) } // DoesCodeReview attempts to determine whether a project requires review before code gets merged. @@ -88,7 +88,7 @@ func DoesCodeReview(c *checker.CheckRequest) checker.CheckResult { "labelsToAnalyze": githubv4.Int(labelsToAnalyze), } if err := c.GraphClient.Query(c.Ctx, &prHistory, vars); err != nil { - return checker.CreateRuntimeErrorResult(checkCodeReview, err) + return checker.CreateRuntimeErrorResult(CheckCodeReview, err) } return checker.MultiCheckOr2( isPrReviewRequired, @@ -139,9 +139,9 @@ func isPrReviewRequired(c *checker.CheckRequest) checker.CheckResult { if prHistory.Repository.DefaultBranchRef.BranchProtectionRule.RequiredApprovingReviewCount >= 1 { // If the default value is 0 when we cannot retrieve the value, // a non-zero value means we're confident it's enabled. - return checker.CreateMaxScoreResult(checkCodeReview, "branch protection for default branch is enabled") + return checker.CreateMaxScoreResult(CheckCodeReview, "branch protection for default branch is enabled") } - return checker.CreateInconclusiveResult(checkCodeReview, "cannot determine if branch protection is enabled") + return checker.CreateInconclusiveResult(CheckCodeReview, "cannot determine if branch protection is enabled") } func prowCodeReview(c *checker.CheckRequest) checker.CheckResult { @@ -167,7 +167,7 @@ func prowCodeReview(c *checker.CheckRequest) checker.CheckResult { func commitMessageHints(c *checker.CheckRequest) checker.CheckResult { commits, _, err := c.Client.Repositories.ListCommits(c.Ctx, c.Owner, c.Repo, &github.CommitsListOptions{}) if err != nil { - return checker.MakeRetryResult(checkCodeReview, err) + return checker.MakeRetryResult(CheckCodeReview, err) } total := 0 @@ -203,7 +203,7 @@ func commitMessageHints(c *checker.CheckRequest) checker.CheckResult { func createResult(reviewName string, reviewed, total int) checker.CheckResult { if total > 0 { reason := fmt.Sprintf("%s code reviews found for %v commits out of the last %v", reviewName, reviewed, total) - return checker.CreateProportionalScoreResult(checkCodeReview, reason, reviewed, total) + return checker.CreateProportionalScoreResult(CheckCodeReview, reason, reviewed, total) } - return checker.CreateInconclusiveResult(checkCodeReview, fmt.Sprintf("no %s commits found", reviewName)) + return checker.CreateInconclusiveResult(CheckCodeReview, fmt.Sprintf("no %s commits found", reviewName)) } diff --git a/checks/frozen_deps.go b/checks/frozen_deps.go index 85b43f026bd..796824e51d4 100644 --- a/checks/frozen_deps.go +++ b/checks/frozen_deps.go @@ -26,8 +26,8 @@ import ( sce "github.com/ossf/scorecard/errors" ) -// checkFrozenDeps is the registered name for FrozenDeps. -const checkFrozenDeps = "Frozen-Deps" +// CheckFrozenDeps is the registered name for FrozenDeps. +const CheckFrozenDeps = "Frozen-Deps" // Structure for workflow config. // We only declare the fields we need. @@ -54,7 +54,7 @@ type gitHubActionWorkflowConfig struct { //nolint:gochecknoinits func init() { - registerCheck(checkFrozenDeps, FrozenDeps) + registerCheck(CheckFrozenDeps, FrozenDeps) } // FrozenDeps will check the repository if it contains frozen dependecies. @@ -78,14 +78,14 @@ func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.Check func createResultForIsShellScriptFreeOfInsecureDownloads(r bool, err error) checker.CheckResult { if err != nil { - return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err) } if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, + return checker.CreateMinScoreResult(CheckFrozenDeps, "insecure (unpinned) dependency downloads found in shell scripts") } - return checker.CreateMaxScoreResult(checkFrozenDeps, + return checker.CreateMaxScoreResult(CheckFrozenDeps, "no insecure (unpinned) dependency downloads found in shell scripts") } @@ -112,14 +112,14 @@ func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckR // Create the result. func createResultForIsDockerfileFreeOfInsecureDownloads(r bool, err error) checker.CheckResult { if err != nil { - return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err) } if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, + return checker.CreateMinScoreResult(CheckFrozenDeps, "insecure (unpinned) dependency downloads found in Dockerfiles") } - return checker.CreateMaxScoreResult(checkFrozenDeps, + return checker.CreateMaxScoreResult(CheckFrozenDeps, "no insecure (unpinned) dependency downloads found in Dockerfiles") } @@ -175,13 +175,13 @@ func isDockerfilePinned(c *checker.CheckRequest) checker.CheckResult { // Create the result. func createResultForIsDockerfilePinned(r bool, err error) checker.CheckResult { if err != nil { - return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err) } if r { - return checker.CreateMaxScoreResult(checkFrozenDeps, "Dockerfile dependencies are pinned") + return checker.CreateMaxScoreResult(CheckFrozenDeps, "Dockerfile dependencies are pinned") } - return checker.CreateMinScoreResult(checkFrozenDeps, "unpinned dependencies found Dockerfiles") + return checker.CreateMinScoreResult(CheckFrozenDeps, "unpinned dependencies found Dockerfiles") } func TestValidateDockerfileIsPinned(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult { @@ -278,14 +278,14 @@ func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) chec // Create the result. func createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r bool, err error) checker.CheckResult { if err != nil { - return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err) } if !r { - return checker.CreateMinScoreResult(checkFrozenDeps, + return checker.CreateMinScoreResult(CheckFrozenDeps, "insecure (unpinned) dependency downloads found in GitHub workflows") } - return checker.CreateMaxScoreResult(checkFrozenDeps, + return checker.CreateMaxScoreResult(CheckFrozenDeps, "no insecure (unpinned) dependency downloads found in GitHub workflows") } @@ -362,13 +362,13 @@ func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) checker.CheckResult // Create the result. func createResultForIsGitHubActionsWorkflowPinned(r bool, err error) checker.CheckResult { if err != nil { - return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err) } if r { - return checker.CreateMaxScoreResult(checkFrozenDeps, "GitHub actions are pinned") + return checker.CreateMaxScoreResult(CheckFrozenDeps, "GitHub actions are pinned") } - return checker.CreateMinScoreResult(checkFrozenDeps, "GitHub actions are not pinned") + return checker.CreateMinScoreResult(CheckFrozenDeps, "GitHub actions are not pinned") } func TestIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult { @@ -415,15 +415,15 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, dl checker.Deta // Check presence of lock files thru validatePackageManagerFile(). func isPackageManagerLockFilePresent(c *checker.CheckRequest) checker.CheckResult { - r, err := CheckIfFileExists2(checkFrozenDeps, c, validatePackageManagerFile) + r, err := CheckIfFileExists2(CheckFrozenDeps, c, validatePackageManagerFile) if err != nil { - return checker.CreateRuntimeErrorResult(checkFrozenDeps, err) + return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err) } if !r { - return checker.CreateInconclusiveResult(checkFrozenDeps, "no lock files detected for a package manager") + return checker.CreateInconclusiveResult(CheckFrozenDeps, "no lock files detected for a package manager") } - return checker.CreateMaxScoreResult(checkFrozenDeps, "lock file detected for a package manager") + return checker.CreateMaxScoreResult(CheckFrozenDeps, "lock file detected for a package manager") } // validatePackageManagerFile will validate the if frozen dependecies file name exists. diff --git a/checks/frozen_deps_test.go b/checks/frozen_deps_test.go index f2372dc1ff4..aa7ea549a3f 100644 --- a/checks/frozen_deps_test.go +++ b/checks/frozen_deps_test.go @@ -27,13 +27,15 @@ import ( func TestGithubWorkflowPinning(t *testing.T) { t.Parallel() - tests := []scut.TestInfo{ + tests := []struct { + name string + filename string + expected scut.TestReturn + }{ { - Name: "Zero size content", - Args: scut.TestArgs{ - Filename: "", - }, - Expected: scut.TestReturn{ + name: "Zero size content", + filename: "", + expected: scut.TestReturn{ Errors: []error{sce.ErrRunFailure}, Score: checker.InconclusiveResultScore, NumberOfWarn: 0, @@ -42,11 +44,9 @@ func TestGithubWorkflowPinning(t *testing.T) { }, }, { - Name: "Pinned workflow", - Args: scut.TestArgs{ - Filename: "./testdata/workflow-pinned.yaml", - }, - Expected: scut.TestReturn{ + name: "Pinned workflow", + filename: "./testdata/workflow-pinned.yaml", + expected: scut.TestReturn{ Errors: nil, Score: checker.MaxResultScore, NumberOfWarn: 0, @@ -55,11 +55,9 @@ func TestGithubWorkflowPinning(t *testing.T) { }, }, { - Name: "Non-pinned workflow", - Args: scut.TestArgs{ - Filename: "./testdata/workflow-not-pinned.yaml", - }, - Expected: scut.TestReturn{ + name: "Non-pinned workflow", + filename: "./testdata/workflow-not-pinned.yaml", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 1, @@ -70,35 +68,36 @@ func TestGithubWorkflowPinning(t *testing.T) { } 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.Run(tt.name, func(t *testing.T) { t.Parallel() var content []byte var err error - if tt.Args.Filename == "" { + if tt.filename == "" { content = make([]byte, 0) } else { - content, err = ioutil.ReadFile(tt.Args.Filename) + content, err = ioutil.ReadFile(tt.filename) if err != nil { panic(fmt.Errorf("cannot read file: %w", err)) } } dl := scut.TestDetailLogger{} - r := TestIsGitHubActionsWorkflowPinned(tt.Args.Filename, content, &dl) - tt.Args.Dl = dl - scut.ValidateTestInfo(t, &tt, &r) + r := TestIsGitHubActionsWorkflowPinned(tt.filename, content, &dl) + scut.ValidateTestReturn2(t, tt.name, &tt.expected, &r, &dl) }) } } func TestDockerfilePinning(t *testing.T) { t.Parallel() - tests := []scut.TestInfo{ + tests := []struct { + name string + filename string + expected scut.TestReturn + }{ { - Name: "Invalid dockerfile", - Args: scut.TestArgs{ - Filename: "./testdata/Dockerfile-invalid", - }, - Expected: scut.TestReturn{ + name: "Invalid dockerfile", + filename: "./testdata/Dockerfile-invalid", + expected: scut.TestReturn{ Errors: []error{sce.ErrRunFailure}, Score: checker.InconclusiveResultScore, NumberOfWarn: 0, @@ -107,11 +106,9 @@ func TestDockerfilePinning(t *testing.T) { }, }, { - Name: "Pinned dockerfile", - Args: scut.TestArgs{ - Filename: "./testdata/Dockerfile-pinned", - }, - Expected: scut.TestReturn{ + name: "Pinned dockerfile", + filename: "./testdata/Dockerfile-pinned", + expected: scut.TestReturn{ Errors: nil, Score: checker.MaxResultScore, NumberOfWarn: 0, @@ -120,11 +117,9 @@ func TestDockerfilePinning(t *testing.T) { }, }, { - Name: "Pinned dockerfile as", - Args: scut.TestArgs{ - Filename: "./testdata/Dockerfile-pinned-as", - }, - Expected: scut.TestReturn{ + name: "Pinned dockerfile as", + filename: "./testdata/Dockerfile-pinned-as", + expected: scut.TestReturn{ Errors: nil, Score: checker.MaxResultScore, NumberOfWarn: 0, @@ -133,11 +128,9 @@ func TestDockerfilePinning(t *testing.T) { }, }, { - Name: "Non-pinned dockerfile as", - Args: scut.TestArgs{ - Filename: "./testdata/Dockerfile-not-pinned-as", - }, - Expected: scut.TestReturn{ + name: "Non-pinned dockerfile as", + filename: "./testdata/Dockerfile-not-pinned-as", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 3, // TODO:fix should be 2 @@ -146,11 +139,9 @@ func TestDockerfilePinning(t *testing.T) { }, }, { - Name: "Non-pinned dockerfile", - Args: scut.TestArgs{ - Filename: "./testdata/Dockerfile-not-pinned", - }, - Expected: scut.TestReturn{ + name: "Non-pinned dockerfile", + filename: "./testdata/Dockerfile-not-pinned", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 1, @@ -161,35 +152,36 @@ func TestDockerfilePinning(t *testing.T) { } 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.Run(tt.name, func(t *testing.T) { t.Parallel() var content []byte var err error - if tt.Args.Filename == "" { + if tt.filename == "" { content = make([]byte, 0) } else { - content, err = ioutil.ReadFile(tt.Args.Filename) + content, err = ioutil.ReadFile(tt.filename) if err != nil { panic(fmt.Errorf("cannot read file: %w", err)) } } dl := scut.TestDetailLogger{} - r := TestValidateDockerfileIsPinned(tt.Args.Filename, content, &dl) - tt.Args.Dl = dl - scut.ValidateTestInfo(t, &tt, &r) + r := TestValidateDockerfileIsPinned(tt.filename, content, &dl) + scut.ValidateTestReturn2(t, tt.name, &tt.expected, &r, &dl) }) } } func TestDockerfileScriptDownload(t *testing.T) { t.Parallel() - tests := []scut.TestInfo{ + tests := []struct { + name string + filename string + expected scut.TestReturn + }{ { - Name: "curl | sh", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-curl-sh", - }, - Expected: scut.TestReturn{ + name: "curl | sh", + filename: "testdata/Dockerfile-curl-sh", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 4, @@ -198,11 +190,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "wget | /bin/sh", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-wget-bin-sh", - }, - Expected: scut.TestReturn{ + name: "wget | /bin/sh", + filename: "testdata/Dockerfile-wget-bin-sh", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 3, @@ -211,11 +201,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "wget no exec", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-script-ok", - }, - Expected: scut.TestReturn{ + name: "wget no exec", + filename: "testdata/Dockerfile-script-ok", + expected: scut.TestReturn{ Errors: nil, Score: checker.MaxResultScore, NumberOfWarn: 0, @@ -224,11 +212,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "curl file sh", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-curl-file-sh", - }, - Expected: scut.TestReturn{ + name: "curl file sh", + filename: "testdata/Dockerfile-curl-file-sh", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 12, @@ -237,11 +223,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "proc substitution", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-proc-subs", - }, - Expected: scut.TestReturn{ + name: "proc substitution", + filename: "testdata/Dockerfile-proc-subs", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 6, @@ -250,11 +234,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "wget file", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-wget-file", - }, - Expected: scut.TestReturn{ + name: "wget file", + filename: "testdata/Dockerfile-wget-file", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 10, @@ -263,11 +245,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "gsutil file", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-gsutil-file", - }, - Expected: scut.TestReturn{ + name: "gsutil file", + filename: "testdata/Dockerfile-gsutil-file", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 17, @@ -276,11 +256,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "aws file", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-aws-file", - }, - Expected: scut.TestReturn{ + name: "aws file", + filename: "testdata/Dockerfile-aws-file", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 15, @@ -289,11 +267,9 @@ func TestDockerfileScriptDownload(t *testing.T) { }, }, { - Name: "pkg managers", - Args: scut.TestArgs{ - Filename: "testdata/Dockerfile-pkg-managers", - }, - Expected: scut.TestReturn{ + name: "pkg managers", + filename: "testdata/Dockerfile-pkg-managers", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 27, @@ -304,35 +280,36 @@ func TestDockerfileScriptDownload(t *testing.T) { } 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.Run(tt.name, func(t *testing.T) { t.Parallel() var content []byte var err error - if tt.Args.Filename == "" { + if tt.filename == "" { content = make([]byte, 0) } else { - content, err = ioutil.ReadFile(tt.Args.Filename) + content, err = ioutil.ReadFile(tt.filename) if err != nil { panic(fmt.Errorf("cannot read file: %w", err)) } } dl := scut.TestDetailLogger{} - r := TestValidateDockerfileIsFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) - tt.Args.Dl = dl - scut.ValidateTestInfo(t, &tt, &r) + r := TestValidateDockerfileIsFreeOfInsecureDownloads(tt.filename, content, &dl) + scut.ValidateTestReturn2(t, tt.name, &tt.expected, &r, &dl) }) } } func TestShellScriptDownload(t *testing.T) { t.Parallel() - tests := []scut.TestInfo{ + tests := []struct { + name string + filename string + expected scut.TestReturn + }{ { - Name: "sh script", - Args: scut.TestArgs{ - Filename: "testdata/script-sh", - }, - Expected: scut.TestReturn{ + name: "sh script", + filename: "testdata/script-sh", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 7, @@ -341,11 +318,9 @@ func TestShellScriptDownload(t *testing.T) { }, }, { - Name: "bash script", - Args: scut.TestArgs{ - Filename: "testdata/script-bash", - }, - Expected: scut.TestReturn{ + name: "bash script", + filename: "testdata/script-bash", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 7, @@ -354,11 +329,9 @@ func TestShellScriptDownload(t *testing.T) { }, }, { - Name: "sh script 2", - Args: scut.TestArgs{ - Filename: "testdata/script.sh", - }, - Expected: scut.TestReturn{ + name: "sh script 2", + filename: "testdata/script.sh", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 7, @@ -367,11 +340,9 @@ func TestShellScriptDownload(t *testing.T) { }, }, { - Name: "pkg managers", - Args: scut.TestArgs{ - Filename: "testdata/script-pkg-managers", - }, - Expected: scut.TestReturn{ + name: "pkg managers", + filename: "testdata/script-pkg-managers", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 24, @@ -382,35 +353,36 @@ func TestShellScriptDownload(t *testing.T) { } 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.Run(tt.name, func(t *testing.T) { t.Parallel() var content []byte var err error - if tt.Args.Filename == "" { + if tt.filename == "" { content = make([]byte, 0) } else { - content, err = ioutil.ReadFile(tt.Args.Filename) + content, err = ioutil.ReadFile(tt.filename) if err != nil { panic(fmt.Errorf("cannot read file: %w", err)) } } dl := scut.TestDetailLogger{} - r := TestValidateShellScriptIsFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) - tt.Args.Dl = dl - scut.ValidateTestInfo(t, &tt, &r) + r := TestValidateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &dl) + scut.ValidateTestReturn2(t, tt.name, &tt.expected, &r, &dl) }) } } func TestGitHubWorflowRunDownload(t *testing.T) { t.Parallel() - tests := []scut.TestInfo{ + tests := []struct { + name string + filename string + expected scut.TestReturn + }{ { - Name: "workflow curl default", - Args: scut.TestArgs{ - Filename: "testdata/github-workflow-curl-default", - }, - Expected: scut.TestReturn{ + name: "workflow curl default", + filename: "testdata/github-workflow-curl-default", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 1, @@ -419,11 +391,9 @@ func TestGitHubWorflowRunDownload(t *testing.T) { }, }, { - Name: "workflow curl no default", - Args: scut.TestArgs{ - Filename: "testdata/github-workflow-curl-no-default", - }, - Expected: scut.TestReturn{ + name: "workflow curl no default", + filename: "testdata/github-workflow-curl-no-default", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 1, @@ -432,11 +402,9 @@ func TestGitHubWorflowRunDownload(t *testing.T) { }, }, { - Name: "wget across steps", - Args: scut.TestArgs{ - Filename: "testdata/github-workflow-wget-across-steps", - }, - Expected: scut.TestReturn{ + name: "wget across steps", + filename: "testdata/github-workflow-wget-across-steps", + expected: scut.TestReturn{ Errors: nil, Score: checker.MinResultScore, NumberOfWarn: 2, @@ -447,22 +415,21 @@ func TestGitHubWorflowRunDownload(t *testing.T) { } 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.Run(tt.name, func(t *testing.T) { t.Parallel() var content []byte var err error - if tt.Args.Filename == "" { + if tt.filename == "" { content = make([]byte, 0) } else { - content, err = ioutil.ReadFile(tt.Args.Filename) + content, err = ioutil.ReadFile(tt.filename) if err != nil { panic(fmt.Errorf("cannot read file: %w", err)) } } dl := scut.TestDetailLogger{} - r := TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(tt.Args.Filename, content, &dl) - tt.Args.Dl = dl - scut.ValidateTestInfo(t, &tt, &r) + r := TestValidateGitHubWorkflowScriptFreeOfInsecureDownloads(tt.filename, content, &dl) + scut.ValidateTestReturn2(t, tt.name, &tt.expected, &r, &dl) }) } } diff --git a/checks/permissions_test.go b/checks/permissions_test.go index ffbd496c8f5..573d08dc4a0 100644 --- a/checks/permissions_test.go +++ b/checks/permissions_test.go @@ -14,13 +14,7 @@ package checks -import ( - "errors" - "fmt" - "io/ioutil" - "testing" -) - +/* func TestGithubTokenPermissions(t *testing.T) { t.Parallel() type args struct { @@ -142,3 +136,4 @@ func TestGithubTokenPermissions(t *testing.T) { }) } } +*/ diff --git a/e2e/active_test.go b/e2e/active_test.go index 6d7959f9634..aa8b6d7ec3e 100644 --- a/e2e/active_test.go +++ b/e2e/active_test.go @@ -16,19 +16,21 @@ package e2e import ( "context" + "fmt" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/checks" + scut "github.com/ossf/scorecard/utests" ) var _ = Describe("E2E TEST:Active", func() { Context("E2E TEST:Validating active status", func() { It("Should return valid active status", func() { - l := log{} - checkRequest := checker.CheckRequest{ + dl := scut.TestDetailLogger{} + req := checker.CheckRequest{ Ctx: context.Background(), Client: ghClient, HTTPClient: httpClient, @@ -36,11 +38,23 @@ var _ = Describe("E2E TEST:Active", func() { Owner: "apache", Repo: "airflow", GraphClient: graphClient, - Logf: l.Logf, + Dlogger: &dl, } - result := checks.IsActive(&checkRequest) + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, + } + result := checks.IsActive(&req) + // UPGRADEv2: to remove. + // Old version. Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) + fmt.Printf("%v", result) + // New version. + Expect(scut.ValidateTestReturn(nil, "active repo", &expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/e2e/automatic_deps_test.go b/e2e/automatic_deps_test.go index 32ec51e69ed..865ea0ac329 100644 --- a/e2e/automatic_deps_test.go +++ b/e2e/automatic_deps_test.go @@ -24,6 +24,7 @@ import ( "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/checks" "github.com/ossf/scorecard/clients/githubrepo" + scut "github.com/ossf/scorecard/utests" ) @@ -60,7 +61,7 @@ var _ = Describe("E2E TEST:Automatic-Dependency-Update", func() { Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) // New version. - Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + Expect(scut.ValidateTestReturn(nil, "dependabot", &expected, &result, &dl)).Should(BeTrue()) }) It("Should return deps are automatically updated for renovatebot", func() { dl := scut.TestDetailLogger{} @@ -90,7 +91,7 @@ var _ = Describe("E2E TEST:Automatic-Dependency-Update", func() { Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) // New version. - Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + Expect(scut.ValidateTestReturn(nil, "renovabot", &expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/e2e/binary_artifacts_test.go b/e2e/binary_artifacts_test.go index 63940d9f73e..9df3a5b1fb8 100644 --- a/e2e/binary_artifacts_test.go +++ b/e2e/binary_artifacts_test.go @@ -60,7 +60,7 @@ var _ = Describe("E2E TEST:Binary-Artifacts", func() { Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) // New version. - Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + Expect(scut.ValidateTestReturn(nil, "no binary artifacts", &expected, &result, &dl)).Should(BeTrue()) }) It("Should return binary artifacts present in source code", func() { dl := scut.TestDetailLogger{} @@ -90,7 +90,7 @@ var _ = Describe("E2E TEST:Binary-Artifacts", func() { Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeFalse()) // New version. - Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + Expect(scut.ValidateTestReturn(nil, " binary artifacts", &expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/e2e/branchprotection_test.go b/e2e/branchprotection_test.go index b8464bbeb34..e691f83dff8 100644 --- a/e2e/branchprotection_test.go +++ b/e2e/branchprotection_test.go @@ -16,19 +16,21 @@ package e2e import ( "context" + "fmt" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/ossf/scorecard/checker" "github.com/ossf/scorecard/checks" + scut "github.com/ossf/scorecard/utests" ) var _ = Describe("E2E TEST:Branch Protection", func() { Context("E2E TEST:Validating branch protection", func() { It("Should fail to return branch protection on other repositories", func() { - l := log{} - checkRequest := checker.CheckRequest{ + dl := scut.TestDetailLogger{} + req := checker.CheckRequest{ Ctx: context.Background(), Client: ghClient, HTTPClient: httpClient, @@ -36,11 +38,23 @@ var _ = Describe("E2E TEST:Branch Protection", func() { Owner: "apache", Repo: "airflow", GraphClient: graphClient, - Logf: l.Logf, + Dlogger: &dl, } - result := checks.BranchProtection(&checkRequest) - Expect(result.Error).ShouldNot(BeNil()) + expected := scut.TestReturn{ + Errors: nil, + Score: checker.MinResultScore, + NumberOfWarn: 1, + NumberOfInfo: 0, + NumberOfDebug: 0, + } + result := checks.BranchProtection(&req) + // UPGRADEv2: to remove. + // Old version. + Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeFalse()) + panic(fmt.Sprintf("%v", result)) + // New version. + Expect(scut.ValidateTestReturn(nil, "branch protection not enabled", &expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/e2e/code_review_test.go b/e2e/code_review_test.go index 13ac7412eb0..48afad94fad 100644 --- a/e2e/code_review_test.go +++ b/e2e/code_review_test.go @@ -54,7 +54,7 @@ var _ = Describe("E2E TEST:CodeReview", func() { Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) // New version. - Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + Expect(scut.ValidateTestReturn(nil, "use code reviews", &expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/e2e/frozen_deps_test.go b/e2e/frozen_deps_test.go index b96a5b3ad3a..cde9d71d084 100644 --- a/e2e/frozen_deps_test.go +++ b/e2e/frozen_deps_test.go @@ -59,7 +59,7 @@ var _ = Describe("E2E TEST:FrozenDeps", func() { Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeFalse()) // New version. - Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + Expect(scut.ValidateTestReturn(nil, "deps not frozen", &expected, &result, &dl)).Should(BeTrue()) }) It("Should return deps are frozen", func() { dl := scut.TestDetailLogger{} @@ -90,7 +90,7 @@ var _ = Describe("E2E TEST:FrozenDeps", func() { Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) // New version. - Expect(scut.ValidateTestReturn(&expected, &result, &dl)).Should(BeTrue()) + Expect(scut.ValidateTestReturn(nil, "deps frozen", &expected, &result, &dl)).Should(BeTrue()) }) }) }) diff --git a/errors/internal.go b/errors/internal.go index e7c8fd18212..1b2844c1f28 100644 --- a/errors/internal.go +++ b/errors/internal.go @@ -25,4 +25,6 @@ var ( ErrInternalFilenameMatch = errors.New("filename match error") ErrInternalEmptyFile = errors.New("empty file") ErrInternalInvalidShellCode = errors.New("invalid shell code") + ErrCommitishNil = errors.New("commitish is nil") + ErrBranchNotFound = errors.New("branch not found") ) diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index 399d9a98d6c..75bba2af74e 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -194,7 +194,7 @@ func (r *ScorecardResult) AsString2(showDetails bool, logLevel zapcore.Level, wr // UPGRADEv2: rename variable. if row.Score2 == checker.InconclusiveResultScore { - x[0] = "Inconclusive" + x[0] = "?" } else { x[0] = fmt.Sprintf("%d", row.Score2) } diff --git a/utests/utlib.go b/utests/utlib.go index c5a204a41bd..56719bdd782 100644 --- a/utests/utlib.go +++ b/utests/utlib.go @@ -47,10 +47,10 @@ type TestDetailLogger struct { messages []checker.CheckDetail } -type TestArgs struct { - Filename string - Dl TestDetailLogger -} +// type TestArgs struct { +// Filename string +// Dl TestDetailLogger +// } type TestReturn struct { Errors []error @@ -60,12 +60,6 @@ type TestReturn struct { NumberOfDebug int } -type TestInfo struct { - Name string - Args TestArgs - Expected TestReturn -} - func (l *TestDetailLogger) Info(desc string, args ...interface{}) { cd := checker.CheckDetail{Type: checker.DetailInfo, Msg: fmt.Sprintf(desc, args...)} l.messages = append(l.messages, cd) @@ -81,41 +75,25 @@ func (l *TestDetailLogger) Debug(desc string, args ...interface{}) { l.messages = append(l.messages, cd) } -func ValidateTestReturn(te *TestReturn, tr *checker.CheckResult, dl *TestDetailLogger) bool { - for _, we := range te.Errors { - if !errors.Is(tr.Error2, we) { - fmt.Printf("invalid error returned: %v is not of type %v", - tr.Error, we) - return false - } - } - // UPGRADEv2: update name. - if tr.Score2 != te.Score || - !validateDetailTypes(dl.messages, te.NumberOfWarn, - te.NumberOfInfo, te.NumberOfDebug) { - return false - } - return true -} - //nolint -func ValidateTestInfo(t *testing.T, ti *TestInfo, tr *checker.CheckResult) bool { - for _, we := range ti.Expected.Errors { +func ValidateTestReturn(t *testing.T, name string, te *TestReturn, + tr *checker.CheckResult, dl *TestDetailLogger) bool { + for _, we := range te.Errors { if !errors.Is(tr.Error2, we) { if t != nil { t.Errorf("%v: invalid error returned: %v is not of type %v", - ti.Name, tr.Error, we) + name, tr.Error, we) } return false } } // UPGRADEv2: update name. - if tr.Score2 != ti.Expected.Score || - !validateDetailTypes(ti.Args.Dl.messages, ti.Expected.NumberOfWarn, - ti.Expected.NumberOfInfo, ti.Expected.NumberOfDebug) { + if tr.Score2 != te.Score || + !validateDetailTypes(dl.messages, te.NumberOfWarn, + te.NumberOfInfo, te.NumberOfDebug) { if t != nil { - t.Errorf("%v: %v. Got (score=%v) expected (%v)\n%v", - ti.Name, ti.Args.Filename, tr.Score2, ti.Expected.Score, ti.Args.Dl.messages) + t.Errorf("%v: Got (score=%v) expected (%v)\n%v", + name, tr.Score2, te.Score, dl.messages) } return false }