mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-17 11:57:12 +03:00
✨ [migration to score] 1: create errors and new functions (#712)
* details-1 * comment * doc * nits * typo * commments * nit * linter
This commit is contained in:
parent
45ea97e502
commit
ab4bb60c9c
@ -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
|
||||
|
||||
|
@ -30,6 +30,8 @@ type CheckRequest struct {
|
||||
GraphClient *githubv4.Client
|
||||
HTTPClient *http.Client
|
||||
RepoClient clients.RepoClient
|
||||
// UPGRADEv2: Logf will be removed.
|
||||
Logf func(s string, f ...interface{})
|
||||
Dlogger DetailLogger
|
||||
Owner, Repo string
|
||||
}
|
||||
|
@ -16,22 +16,191 @@ package checker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
scorecarderrors "github.com/ossf/scorecard/errors"
|
||||
)
|
||||
|
||||
const MaxResultConfidence = 10
|
||||
// UPGRADEv2: to remove.
|
||||
const (
|
||||
MaxResultConfidence = 10
|
||||
HalfResultConfidence = 5
|
||||
MinResultConfidence = 0
|
||||
)
|
||||
|
||||
// UPGRADEv2: to remove.
|
||||
const migrationThresholdPassValue = 8
|
||||
|
||||
// ErrorDemoninatorZero indicates the denominator for a proportional result is 0.
|
||||
// UPGRADEv2: to remove.
|
||||
var ErrorDemoninatorZero = errors.New("internal error: denominator is 0")
|
||||
|
||||
// Types of details.
|
||||
type DetailType int
|
||||
|
||||
const (
|
||||
DetailInfo DetailType = iota
|
||||
DetailWarn
|
||||
DetailDebug
|
||||
)
|
||||
|
||||
// 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..
|
||||
}
|
||||
|
||||
type DetailLogger interface {
|
||||
Info(desc string, args ...interface{})
|
||||
Warn(desc string, args ...interface{})
|
||||
Debug(desc string, args ...interface{})
|
||||
}
|
||||
|
||||
//nolint
|
||||
const (
|
||||
MaxResultScore = 10
|
||||
MinResultScore = 0
|
||||
InconclusiveResultScore = -1
|
||||
)
|
||||
|
||||
//nolint
|
||||
type CheckResult struct {
|
||||
// Old structure
|
||||
Error error `json:"-"`
|
||||
Name string
|
||||
Details []string
|
||||
Confidence int
|
||||
Pass bool
|
||||
ShouldRetry bool `json:"-"`
|
||||
|
||||
// UPGRADEv2: New structure. Omitting unchanged Name field
|
||||
// for simplicity.
|
||||
Version int `json:"-"` // 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
|
||||
Score int `json:"-"` // {[-1,0...10], -1 = Inconclusive}
|
||||
Reason string `json:"-"` // A sentence describing the check result (score, etc)
|
||||
}
|
||||
|
||||
// CreateResultWithScore is used when
|
||||
// the check runs without runtime errors and we want to assign a
|
||||
// specific score.
|
||||
func CreateResultWithScore(name, reason string, score int) CheckResult {
|
||||
pass := true
|
||||
if score < migrationThresholdPassValue {
|
||||
pass = false
|
||||
}
|
||||
return CheckResult{
|
||||
Name: name,
|
||||
// Old structure.
|
||||
Error: nil,
|
||||
Confidence: MaxResultScore,
|
||||
Pass: pass,
|
||||
ShouldRetry: false,
|
||||
// New structure.
|
||||
//nolint
|
||||
Version: 2,
|
||||
Error2: nil,
|
||||
Score: score,
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
score := int(math.Min(float64(MaxResultScore*b/t), float64(MaxResultScore)))
|
||||
if score < migrationThresholdPassValue {
|
||||
pass = false
|
||||
}
|
||||
return CheckResult{
|
||||
Name: name,
|
||||
// Old structure.
|
||||
Error: nil,
|
||||
Confidence: MaxResultConfidence,
|
||||
Pass: pass,
|
||||
ShouldRetry: false,
|
||||
// New structure.
|
||||
//nolint
|
||||
Version: 2,
|
||||
Error2: nil,
|
||||
Score: score,
|
||||
Reason: fmt.Sprintf("%v -- score normalized to %d", reason, score),
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
// Old structure.
|
||||
Confidence: 0,
|
||||
Pass: false,
|
||||
ShouldRetry: false,
|
||||
// New structure.
|
||||
//nolint
|
||||
Version: 2,
|
||||
Score: InconclusiveResultScore,
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
// Old structure.
|
||||
Error: e,
|
||||
Confidence: 0,
|
||||
Pass: false,
|
||||
ShouldRetry: false,
|
||||
// New structure.
|
||||
//nolint
|
||||
Version: 2,
|
||||
Error2: e,
|
||||
Score: InconclusiveResultScore,
|
||||
Reason: e.Error(), // Note: message already accessible by caller thru `Error`.
|
||||
}
|
||||
}
|
||||
|
||||
// UPGRADEv2: will be renaall functions belowed will be removed.
|
||||
func MakeAndResult2(checks ...CheckResult) CheckResult {
|
||||
if len(checks) == 0 {
|
||||
// That should never happen.
|
||||
panic("MakeResult called with no checks")
|
||||
}
|
||||
|
||||
worseResult := checks[0]
|
||||
// UPGRADEv2: will go away after old struct is removed.
|
||||
//nolint
|
||||
for _, result := range checks[1:] {
|
||||
if result.Score < worseResult.Score {
|
||||
worseResult = result
|
||||
}
|
||||
}
|
||||
return worseResult
|
||||
}
|
||||
|
||||
func MakeInconclusiveResult(name string, err error) CheckResult {
|
||||
@ -39,7 +208,7 @@ func MakeInconclusiveResult(name string, err error) CheckResult {
|
||||
Name: name,
|
||||
Pass: false,
|
||||
Confidence: 0,
|
||||
Error: scorecarderrors.MakeZeroConfidenceError(err),
|
||||
Error: scorecarderrors.MakeLowConfidenceError(err),
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +217,7 @@ func MakePassResult(name string) CheckResult {
|
||||
Name: name,
|
||||
Pass: true,
|
||||
Confidence: MaxResultConfidence,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,6 +268,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
|
||||
@ -117,7 +288,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
|
||||
|
@ -39,15 +39,34 @@ type CheckFn func(*CheckRequest) CheckResult
|
||||
|
||||
type CheckNameToFnMap map[string]CheckFn
|
||||
|
||||
// UPGRADEv2: messages2 will ultimately
|
||||
// be renamed to messages.
|
||||
type logger struct {
|
||||
messages []string
|
||||
messages []string
|
||||
messages2 []CheckDetail
|
||||
}
|
||||
|
||||
func (l *logger) Info(desc string, args ...interface{}) {
|
||||
cd := CheckDetail{Type: DetailInfo, Msg: fmt.Sprintf(desc, args...)}
|
||||
l.messages2 = append(l.messages2, cd)
|
||||
}
|
||||
|
||||
func (l *logger) Warn(desc string, args ...interface{}) {
|
||||
cd := CheckDetail{Type: DetailWarn, Msg: fmt.Sprintf(desc, args...)}
|
||||
l.messages2 = append(l.messages2, cd)
|
||||
}
|
||||
|
||||
func (l *logger) Debug(desc string, args ...interface{}) {
|
||||
cd := CheckDetail{Type: DetailDebug, Msg: fmt.Sprintf(desc, args...)}
|
||||
l.messages2 = append(l.messages2, cd)
|
||||
}
|
||||
|
||||
// UPGRADEv2: to remove.
|
||||
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))
|
||||
|
||||
@ -74,17 +93,22 @@ func (r *Runner) Run(ctx context.Context, f CheckFn) CheckResult {
|
||||
checkRequest := r.CheckRequest
|
||||
checkRequest.Ctx = ctx
|
||||
l = logger{}
|
||||
// UPGRADEv2: to remove.
|
||||
checkRequest.Logf = l.Logf
|
||||
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") {
|
||||
checkRequest.Logf("error, retrying: %s", res.Error)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
// UPGRADEv2: to remove.
|
||||
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
|
||||
@ -97,6 +121,39 @@ func Bool2int(b bool) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// UPGRADEv2: will be renamed.
|
||||
func MultiCheckOr2(fns ...CheckFn) CheckFn {
|
||||
return func(c *CheckRequest) CheckResult {
|
||||
//nolint
|
||||
maxResult := CheckResult{Version: 2}
|
||||
|
||||
for _, fn := range fns {
|
||||
result := fn(c)
|
||||
|
||||
if result.Score > maxResult.Score {
|
||||
maxResult = result
|
||||
}
|
||||
|
||||
if maxResult.Score >= MaxResultScore {
|
||||
break
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
@ -27,8 +27,8 @@ func TestMakeCheckAnd(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
checks []CheckResult
|
||||
want CheckResult
|
||||
checks []CheckResult
|
||||
}{
|
||||
{
|
||||
name: "Multiple passing",
|
||||
|
@ -88,6 +88,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { //nolint:tparallel // mock
|
||||
rel1 := "release/v.1"
|
||||
sha := "8fb3cb86082b17144a80402f5367ae65f06083bd"
|
||||
main := "main"
|
||||
//nolint
|
||||
tests := []struct {
|
||||
name string
|
||||
branches []*string
|
||||
|
33
checks/write.md
Normal file
33
checks/write.md
Normal file
@ -0,0 +1,33 @@
|
||||
# How to write a check
|
||||
The steps to writting a check are as follow:
|
||||
|
||||
1. Create a file under `checks/` folder, say `checks/mycheck.go`
|
||||
2. Give the check a name and register the check:
|
||||
```
|
||||
// Note: export the name: start its name with an upper-case letter.
|
||||
const CheckMyCheckName string = "My-Check"
|
||||
|
||||
func init() {
|
||||
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 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 determine a score, use `checker.CreateInconclusiveResult()` function.
|
||||
* For proportional results, use `checker.CreateProportionalScoreResult()`.
|
||||
* 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 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.
|
||||
|
||||
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).
|
@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// UPGRADEv2: use ErrRepoUnreachable instead.
|
||||
type ErrRepoUnavailable struct {
|
||||
innerError error
|
||||
}
|
||||
|
14
cmd/root.go
14
cmd/root.go
@ -67,6 +67,9 @@ or ./scorecard --{npm,pypi,rubgems}=<package_name> [--checks=check1,...] [--show
|
||||
Short: "Security Scorecards",
|
||||
Long: "A program that shows security scorecard for an open source software.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// UPGRADEv2: to remove.
|
||||
_, v2 := os.LookupEnv("SCORECARD_V2")
|
||||
fmt.Printf("*** USING v2 MIGRATION CODE ***\n\n")
|
||||
cfg := zap.NewProductionConfig()
|
||||
cfg.Level.SetLevel(*logLevel)
|
||||
logger, err := cfg.Build()
|
||||
@ -157,11 +160,16 @@ or ./scorecard --{npm,pypi,rubgems}=<package_name> [--checks=check1,...] [--show
|
||||
|
||||
switch format {
|
||||
case formatDefault:
|
||||
err = repoResult.AsString(showDetails, os.Stdout)
|
||||
// UPGRADEv2: to remove.
|
||||
if v2 {
|
||||
err = repoResult.AsString2(showDetails, *logLevel, os.Stdout)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
@ -89,7 +90,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)
|
||||
}
|
||||
}
|
||||
|
28
errors/errors.md
Normal file
28
errors/errors.md
Normal file
@ -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 checks/errors.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 error 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 an API call error.
|
||||
err := dependency.apiCall()
|
||||
if err != nil {
|
||||
return sce.Create(sce.ErrRunFailure, fmt.Sprintf("%v: %v", 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))
|
||||
}
|
||||
```
|
@ -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
|
||||
}
|
||||
|
38
errors/public.go
Normal file
38
errors/public.go
Normal file
@ -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 (
|
||||
ErrScorecardInternal = errors.New("internal error")
|
||||
ErrRepoUnreachable = errors.New("repo unreachable")
|
||||
)
|
||||
|
||||
// Create a public error using any of the errors
|
||||
// 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 {
|
||||
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)
|
||||
}
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/ossf/scorecard/checker"
|
||||
)
|
||||
@ -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 {
|
||||
@ -51,6 +52,8 @@ func (r *ScorecardResult) AsJSON(showDetails bool, writer io.Writer) error {
|
||||
Date: r.Date,
|
||||
Metadata: r.Metadata,
|
||||
}
|
||||
// UPGRADEv2: remove nolint after uggrade.
|
||||
//nolint
|
||||
for _, checkResult := range r.Checks {
|
||||
tmpResult := checker.CheckResult{
|
||||
Name: checkResult.Name,
|
||||
@ -65,10 +68,12 @@ 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"}
|
||||
// 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),
|
||||
@ -89,8 +94,10 @@ func (r *ScorecardResult) AsCSV(showDetails bool, writer io.Writer) error {
|
||||
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))
|
||||
//nolint
|
||||
for i, checkResult := range r.Checks {
|
||||
sortedChecks[i] = checkResult
|
||||
}
|
||||
@ -102,6 +109,7 @@ func (r *ScorecardResult) AsString(showDetails bool, writer io.Writer) error {
|
||||
})
|
||||
|
||||
data := make([][]string, len(sortedChecks))
|
||||
//nolint
|
||||
for i, row := range sortedChecks {
|
||||
const withdetails = 4
|
||||
const withoutdetails = 3
|
||||
@ -117,7 +125,16 @@ 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")
|
||||
//nolint
|
||||
if row.Version == 2 {
|
||||
details, show := detailsToString(row.Details2, logLevel)
|
||||
if !show {
|
||||
continue
|
||||
}
|
||||
x[3] = details
|
||||
} else {
|
||||
x[3] = strings.Join(row.Details, "\n")
|
||||
}
|
||||
}
|
||||
data[i] = x
|
||||
}
|
||||
@ -135,9 +152,114 @@ func (r *ScorecardResult) AsString(showDetails bool, writer io.Writer) error {
|
||||
table.SetCenterSeparator("|")
|
||||
table.AppendBulk(data)
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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))
|
||||
//nolint
|
||||
// UPGRADEv2: not needed after upgrade.
|
||||
for i, row := range sortedChecks {
|
||||
//nolint
|
||||
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.Score == checker.InconclusiveResultScore {
|
||||
x[0] = "?"
|
||||
} else {
|
||||
x[0] = fmt.Sprintf("%d", row.Score)
|
||||
}
|
||||
|
||||
doc := fmt.Sprintf("https://github.com/ossf/scorecard/blob/main/checks/checks.md#%s", strings.ToLower(row.Name))
|
||||
x[1] = row.Reason
|
||||
x[2] = row.Name
|
||||
if showDetails {
|
||||
details, show := detailsToString(row.Details2, logLevel)
|
||||
if !show {
|
||||
continue
|
||||
}
|
||||
x[3] = details
|
||||
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/Remediation")
|
||||
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 detailsToString(details []checker.CheckDetail, logLevel zapcore.Level) (string, bool) {
|
||||
// UPGRADEv2: change to make([]string, len(details))
|
||||
// followed by sa[i] = instead of append.
|
||||
//nolint
|
||||
var sa []string
|
||||
for _, v := range details {
|
||||
if v.Type == checker.DetailDebug && logLevel != zapcore.DebugLevel {
|
||||
continue
|
||||
}
|
||||
sa = append(sa, fmt.Sprintf("%s: %s", typeToString(v.Type), v.Msg))
|
||||
}
|
||||
return strings.Join(sa, "\n"), len(sa) > 0
|
||||
}
|
||||
|
||||
func typeToString(cd checker.DetailType) string {
|
||||
switch cd {
|
||||
default:
|
||||
panic("invalid detail")
|
||||
case checker.DetailInfo:
|
||||
return "Info"
|
||||
case checker.DetailWarn:
|
||||
return "Warn"
|
||||
case checker.DetailDebug:
|
||||
return "Debug"
|
||||
}
|
||||
}
|
||||
|
||||
// UPGRADEv2: not needed after upgrade.
|
||||
func displayResult(result bool) string {
|
||||
if result {
|
||||
return "Pass"
|
||||
|
Loading…
Reference in New Issue
Block a user