Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Improve check failure/success details #650

Closed
wants to merge 31 commits into from

Conversation

laurentsimon
Copy link
Contributor

@laurentsimon laurentsimon commented Jul 3, 2021

  • Please check if the PR fulfills these requirements
  • What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
    feature

  • What is the current behavior? (You can also link to an open issue here)
    it's hard for user to have information about failures, and where to find remediation steps.

  • What is the new behavior (if this is a feature change)?

WARNING: I'madding everyone as reviewer to get enough feedback!

This PR adds more information for each check failure. This PR only shows a PoC for 4 of our current checks. The functions that needed change have been duplicated as funcName2; and variables changed to varName2 for this PoC. The changes will happen progressively. Once all the checks have been updated, we can rename the variables to their original names.

For each failure, an error number is shown. The user can use this number to get more information about the failure via, for example:
scorecard --explain Automatic-Dependency-Update-E01

(This is inspired by the Rust toolchain).

I have not implemented this --explain functionality yet. I'll do that when we are in agreement about everything else in this PR.

An error number is returned for every failure in a check: this allows us to have different error numbers for sub-checks: dockerfile pinning, curl | bash, etc within a single check. Note that some checks have a single failure. Checks that use MultiCheckAnd will have multiple sub-checks.

Note that if the check only perform one check, it may feel award to have specific error numbers. We could have

`scorecard --explain Automatic-Dependency-Update`

for certain checks. The current code already supports this. it suffices to never add log details in the check, and the error message above will automatically be updated (see pkg/scorecard_result.go file). The yaml file, as currently updated, will also make this work automatically.

Example of output:

Repo: github.com/a1ive/grub2-filemanager
Frozen-Deps: Fail (Confidence=10)
 FAIL/LockFile: no lock file found in the repo.
   For more information about this failure, try `scorecard --explain Automatic-Dependency-Update/LockFile`
 FAIL/GitHubActions: .github/workflows/build.yml has non-pinned dependency 'actions/checkout@v1' (job 'build').
   For more information about this failure, try `scorecard --explain Automatic-Dependency-Update/GitHubActions`
 FAIL/GitHubActions: .github/workflows/build.yml has non-pinned dependency 'marvinpinto/action-automatic-releases@latest' (job 'build').
 FAIL/GitHubActions: .github/workflows/sync_gitee.yml has non-pinned dependency 'actions/checkout@v1' (job 'build').
 PASS/Dockerfile: Dockerfile dependencies are pinned.
   For more information about this result, try `scorecard --explain Automatic-Dependency-Update/Dockerfile`
 PASS/BinaryDownload: no binary downloads found in Dockerfiles.
   For more information about this result, try `scorecard --explain Automatic-Dependency-Update/BinaryDownload`
 PASS/BinaryDownload: no binary download found in GitHub workflows.
go run . --repo=https://github.com/ossf/scorecard --checks Automatic-Dependency-Update --show-details
Starting [Automatic-Dependency-Update]
Finished [Automatic-Dependency-Update]

RESULTS
-------
Repo: github.com/ossf/scorecard
Automatic-Dependency-Update: Pass (Confidence=10)
 PASS: dependabot config found: .github/dependabot.yml.
For more information about this result, try `scorecard --explain Automatic-Dependency-Update`
go run . --repo=https://github.com/ossf/scorecard --checks Binary-Artifacts --show-details
Starting [Binary-Artifacts]
Finished [Binary-Artifacts]

RESULTS
-------
Repo: github.com/ossf/scorecard
Binary-Artifacts: Pass (Confidence=10)
 PASS: no binary files found in the repo.
For more information about this result, try `scorecard --explain Binary-Artifacts`

It's easy to adapt the code to save into a SARIF format or Json, since there is enough information available in the details and in the yaml file. If we want to display full remediation steps, e.g. for scorecard --explain Automatic-Dependency-Update-E01, we can load the info from the yaml file.

Please read some of the comments I added in the PR.

checker/check_result.go Outdated Show resolved Hide resolved
ShouldRetry: true,
Error: scorecarderrors.MakeRetryError(err),
}
}

// TODO: update this function to return a ResultDontKnow
// if the confidence is low?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this decision be left to the check itself, rather than trying to decide for all checks here? Since checks are very different, I think it's hard to do that in this function in practice.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can pass 2 extra args here resultWhenBelowThreshold and resultWhenAboveThreshold.

checker/check_result.go Outdated Show resolved Hide resolved
checker/check_runner.go Outdated Show resolved Hide resolved
checks/checks2.yaml Outdated Show resolved Hide resolved
@@ -20,32 +20,38 @@ import (
"github.com/ossf/scorecard/checker"
)

const CheckAutomaticDependencyUpdate = "Automatic-Dependency-Update"
const checkAutomaticDependencyUpdate = "Automatic-Dependency-Update"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are choosing to stop exporting these check names?

Copy link
Contributor Author

@laurentsimon laurentsimon Jul 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these variables are not exported and not used outside check implementations. They are passed only as arguments to registerCheck, so I've renamed them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks. Will revert this hen.

@@ -25,12 +26,44 @@ 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we can capture all info here (from line 29-58) using custom errors.

type ScorecardError struct {
  ErrorLevel int  // enum values: Info, Warn, Log
  ErrorType  int  // enum values: Pass, Fail, NA
}
func (e ScorecardError) Error() string {}
func (e ScorecardError) Unwrap() error {}

We then update CheckResult to have a new fieldErrors []ScorecardError or Errors []error. This will contain all failures from a single check.

This is also useful when we expose Scorecard as a library, since the caller can propagate/handle these like regular Golang errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@laurentsimon laurentsimon Jul 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we can capture all info here (from line 29-58) using custom errors.

type ScorecardError struct {
  ErrorLevel int  // enum values: Info, Warn, Log
  ErrorType  int  // enum values: Pass, Fail, NA

I'm not sure this always works: some warning are intended for the user and don't indicate failures or success per-se. For example, if we log every commit that is reviewed, eg code_review.go: it indicates neither failure, nor success, nor NA. That's why I treated them as an enum.

}
func (e ScorecardError) Error() string {}
func (e ScorecardError) Unwrap() error {}


We then update `CheckResult` to have a new field`Errors []ScorecardError` or `Errors []error`. This will contain all failures from a single check.

This is also useful when we expose Scorecard as a library, since the caller can propagate/handle these like regular Golang errors.

I did not do it this way because using errors for failed passes conflates runtime errors with results. What I've implemented is equivalent to:

details, err := runCheck()
if err != nil {
   // we could not run the check
}
// check was run, we can now inspect the results

AFAIK, errors should indicate a runtime error to perform the check: for example we cannot parse a file, we cannot parse an API response, the repo does not exist, the API response indicates an error, etc

In contrast, check failures/passes are just results the API caller can look at, but there's no need to 'propagate' them like errors (the caller is free to use errors if they want, though). The result has everything needed by the caller:

  1. runtime error (see below) Error
  2. result Pass2: Fail/Pass/NA only relevant if no runtime error.
  3. details about what failed Details2, what passed, etc. The caller can inspect the structure if they want more information. The check name and the error name (if there are sub-checks) can be used to show remediation from yaml file, for example.

Notes on runtime errors:
Currently, we propagate all errors we encounter: for example frozen_deps.go, binary_artifact.go, code_review.go But I think this is wrong because it exposes low-level implementation details the caller should not rely upon. This is called out in https://blog.golang.org/go1.13-errors.
If we ever change the implementation, our callers will all break. To be clear, I started doing this myself so I'm the main culprit :-)

I'll send a follow-up PoC PR with suggestions on error handling so we can discuss it more. (by error, I mean runtime errors).

Copy link
Contributor

@oliverchang oliverchang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One high level comment: There's no context passed in between the scorecard run and the proposed --explain apart from an error code.

It seems beneficial for the detailed error/remediation messages to be a template etc, where context from the actual check run is passed through? Some context might be included in the log message with the CLogger.Fail call, but it seems better for UX if we just merge this together with the --explain output to provide a single cohesive set of remediation steps rather than 2 separate ones that a human has to mentally connect?

Do we need a separate step (--explain) to show the detailed error? We can also avoid non-descriptive error codes if we don't have this indirection.

checks/checks2.yaml Outdated Show resolved Hide resolved
checks/checks2.yaml Outdated Show resolved Hide resolved
@laurentsimon
Copy link
Contributor Author

laurentsimon commented Jul 7, 2021

One high level comment: There's no context passed in between the scorecard run and the proposed --explain apart from an error code.

It seems beneficial for the detailed error/remediation messages to be a template etc, where context from the actual check run is passed through? Some context might be included in the log message with the CLogger.Fail call, but it seems better for UX if we just merge this together with the --explain output to provide a single cohesive set of remediation steps rather than 2 separate ones that a human has to mentally connect?

Do we need a separate step (--explain) to show the detailed error? We can also avoid non-descriptive error codes if we don't have this indirection.

I don't know the answer. Having all in one place has it pros like you mention, but it also bloats the output.
To reduce the bloat, we could color-code the information: red for failures, green for passes, purple for NA, some other color for remediations steps, etc. Wdut?

About the need for --explain or not: we could leave it up to the user to tell us what they want:
scorecard --repo ... --show-remediation or

scorecard --repo ... 
scorecard --explain CheckName/FailName

Note that even checks that pass may need some explanation: users don't always understand why the check succeeds or has low confidence. The logging we add is explanatory to the user, and scorecard --explain lets users get further details on the check without the need for a web browser or a url to visit (like a man scorecard).

Let's discuss more in the next meeting.

@oliverchang
Copy link
Contributor

One high level comment: There's no context passed in between the scorecard run and the proposed --explain apart from an error code.
It seems beneficial for the detailed error/remediation messages to be a template etc, where context from the actual check run is passed through? Some context might be included in the log message with the CLogger.Fail call, but it seems better for UX if we just merge this together with the --explain output to provide a single cohesive set of remediation steps rather than 2 separate ones that a human has to mentally connect?
Do we need a separate step (--explain) to show the detailed error? We can also avoid non-descriptive error codes if we don't have this indirection.

I don't know the answer. Having all in one place has it pros like you mention, but it also bloats the output.
To reduce the bloat, we could color-code the information: red for failures, green for passes, purple for NA, some other color for remediations steps, etc. Wdut?

Yeah, colour coding or some other way of organising info should work here. I'm not sure bloat is a strong argument against this. It seems much more annoying for an end user if they have to issue many commands (1 + N explain commands) and mentally connect the results to get a full picture of what's going on.

@laurentsimon
Copy link
Contributor Author

@oliverchang @azeemsgoogle I've addressed the comments and implemented what we discussed offline. I'm working on unit tests, but you can review already. Example of output:

go run . --repo=github.com/googleapis/gaxios --checks Frozen-Deps,Binary-Artifacts,Automatic-Dependency-Update,Code-Review --show-details --v2
Starting [Code-Review]
Starting [Frozen-Deps]
Starting [Binary-Artifacts]
Starting [Automatic-Dependency-Update]
Finished [Automatic-Dependency-Update]
Finished [Code-Review]
Finished [Frozen-Deps]
Finished [Binary-Artifacts]

RESULTS
-------
|-------|--------------------------------|-----------------------------|----------------------------------------|------------------------------------------------------------------------------------------|
| SCORE |             REASON             |            NAME             |                DETAILS                 |                                 DOCUMENTATION/REMDIATION                                 |
|-------|--------------------------------|-----------------------------|----------------------------------------|------------------------------------------------------------------------------------------|
| 10    | tool detected                  | Automatic-Dependency-Update |  Info: renovate detected:              | https://github.com/ossf/scorecard/blob/main/checks/checks.md#automatic-dependency-update |
|       |                                |                             | renovate.json                          |                                                                                          |
|-------|--------------------------------|-----------------------------|----------------------------------------|------------------------------------------------------------------------------------------|
| 10    | no binaries found in the repo  | Binary-Artifacts            |                                        | https://github.com/ossf/scorecard/blob/main/checks/checks.md#binary-artifacts            |
|-------|--------------------------------|-----------------------------|----------------------------------------|------------------------------------------------------------------------------------------|
| 10    | GitHub code reviews found for  | Code-Review                 |                                        | https://github.com/ossf/scorecard/blob/main/checks/checks.md#code-review                 |
|       | 30 commits out of the last 30  |                             |                                        |                                                                                          |
|       | -- code normalized to 10       |                             |                                        |                                                                                          |
|-------|--------------------------------|-----------------------------|----------------------------------------|------------------------------------------------------------------------------------------|
| 0     | GitHub actions are not pinned  | Frozen-Deps                 |  Warn: unpinned dependency detected    | https://github.com/ossf/scorecard/blob/main/checks/checks.md#frozen-deps                 |
|       |                                |                             | in .github/workflows/ci.yaml:          |                                                                                          |
|       |                                |                             | 'actions/checkout@v2' (job 'lint')     |                                                                                          |
|       |                                |                             | Warn: unpinned dependency detected     |                                                                                          |
|       |                                |                             | in .github/workflows/ci.yaml:          |                                                                                          |
|       |                                |                             | 'actions/setup-node@v1' (job 'lint')   |                                                                                          |
|       |                                |                             | Warn: unpinned dependency detected     |                                                                                          |
|       |                                |                             | in .github/workflows/ci.yaml:          |                                                                                          |

# 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:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@inferno-chromium @oliverchang wdut of the risk levels I gave?

@laurentsimon
Copy link
Contributor Author

closing since it's superseded by other commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants