[migration to score] 1: create errors and new functions (#712)

* details-1

* comment

* doc

* nits

* typo

* commments

* nit

* linter
This commit is contained in:
laurentsimon 2021-07-20 11:36:35 -07:00 committed by GitHub
parent 45ea97e502
commit ab4bb60c9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 496 additions and 43 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -27,8 +27,8 @@ func TestMakeCheckAnd(t *testing.T) {
t.Parallel()
tests := []struct {
name string
checks []CheckResult
want CheckResult
checks []CheckResult
}{
{
name: "Multiple passing",

View File

@ -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
View 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).

View File

@ -18,6 +18,7 @@ import (
"fmt"
)
// UPGRADEv2: use ErrRepoUnreachable instead.
type ErrRepoUnavailable struct {
innerError error
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
View 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))
}
```

View File

@ -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
View 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)
}

View File

@ -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,
},
}

View File

@ -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"