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

🐛 Surface workflow verification errors with doc link #408

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 11 additions & 23 deletions app/server/post_results.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ var (
errCertMissingURI = errors.New("certificate has no URIs")
errCertWorkflowPathEmpty = errors.New("cert workflow path is empty")
errMismatchedCertAndRequest = errors.New("repository and branch of cert doesn't match that of request")
errNotDefaultBranch = errors.New("branch of cert isn't the repo's default branch")
)

type certInfo struct {
Expand Down Expand Up @@ -108,21 +109,11 @@ func PostResultsHandler(params results.PostResultParams) middleware.Responder {
if err == nil {
return results.NewPostResultCreated().WithPayload("successfully verified and published ScorecardResult")
}
if errors.Is(err, errMismatchedCertAndRequest) ||
errors.Is(err, errGlobalVarsOrDefaults) ||
errors.Is(err, errGlobalWriteAll) ||
errors.Is(err, errGlobalWrite) ||
errors.Is(err, errScorecardJobNotFound) ||
errors.Is(err, errNonScorecardJobHasTokenWrite) ||
errors.Is(err, errJobHasContainerOrServices) ||
errors.Is(err, errScorecardJobRunsOn) ||
errors.Is(err, errUnallowedStepName) ||
errors.Is(err, errScorecardJobEnvVars) ||
errors.Is(err, errScorecardJobDefaults) ||
errors.Is(err, errEmptyStepUses) {
var vErr verificationError
if errors.As(err, &vErr) {
return results.NewPostResultBadRequest().WithPayload(&models.Error{
Code: http.StatusBadRequest,
Message: err.Error(),
Message: vErr.Error(),
})
}
log.Println(err)
Expand All @@ -146,11 +137,11 @@ func processRequest(host, org, repo string, scorecardResult *models.VerifiedScor
if info.repoFullName != fullName(org, repo) ||
(info.repoBranchRef != scorecardResult.Branch &&
info.repoBranchRef != fmt.Sprintf("refs/heads/%s", scorecardResult.Branch)) {
return fmt.Errorf("%w", errMismatchedCertAndRequest)
return verificationError{e: errMismatchedCertAndRequest}
}

if err := getAndVerifyWorkflowContent(ctx, scorecardResult, info); err != nil {
return fmt.Errorf("error verifying workflow: %w", err)
return fmt.Errorf("workflow verification failed: %w", err)
}

// Save scorecard results (results.json, score.txt) to GCS
Expand Down Expand Up @@ -210,7 +201,7 @@ func getAndVerifyWorkflowContent(ctx context.Context,
defaultBranch := repoClient.GetDefaultBranch()
if scorecardResult.Branch != defaultBranch &&
scorecardResult.Branch != fmt.Sprintf("refs/heads/%s", defaultBranch) {
return fmt.Errorf("branch of cert isn't the repo's default branch")
return verificationError{e: errNotDefaultBranch}
}

// Use the cert commit SHA if the workflow file is in the repo being analyzed.
Expand All @@ -222,19 +213,16 @@ func getAndVerifyWorkflowContent(ctx context.Context,

contents, _, _, err := client.Repositories.GetContents(ctx, org, repo, path, opts)
if err != nil {
return fmt.Errorf("error downloading workflow contents from repo: %v", err)
return fmt.Errorf("error downloading workflow contents from repo: %w", err)
}

workflowContent, err := contents.GetContent()
if err != nil {
return fmt.Errorf("error decoding workflow contents: %v", err)
return fmt.Errorf("error decoding workflow contents: %w", err)
}

// Verify scorecard workflow.
if err := verifyScorecardWorkflow(workflowContent); err != nil {
return fmt.Errorf("workflow could not be verified: %v", err)
}
return nil
return verifyScorecardWorkflow(workflowContent)
}

func writeToBlobStore(ctx context.Context, bucketURL, filename string, data []byte) error {
Expand Down Expand Up @@ -285,7 +273,7 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte) (*x509.
return nil, fmt.Errorf("error extracting certificate from entry: %w", err)
}
if len(certs) > 1 {
return nil, fmt.Errorf("%w", errMultipleCerts)
return nil, errMultipleCerts
}

cert := certs[0]
Expand Down
40 changes: 28 additions & 12 deletions app/server/verify_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
"github.com/rhysd/actionlint"
)

const (
workflowRestrictionLink = "https://github.com/ossf/scorecard-action#workflow-restrictions"
)

var (
errActionlintParse = errors.New("errors during actionlint.Parse")
errGlobalVarsOrDefaults = errors.New("workflow contains global env vars or defaults")
Expand All @@ -47,6 +51,18 @@ var ubuntuRunners = map[string]bool{
"ubuntu-18.04": true,
}

type verificationError struct {
e error
}

func (ve verificationError) Error() string {
return fmt.Sprintf("workflow verification failed: %v, see %s for details.", ve.e, workflowRestrictionLink)
}

func (ve verificationError) Unwrap() error {
return ve.e
}

func verifyScorecardWorkflow(workflowContent string) error {
// Verify workflow contents using actionlint.
workflow, lintErrs := actionlint.Parse([]byte(workflowContent))
Expand All @@ -56,49 +72,49 @@ func verifyScorecardWorkflow(workflowContent string) error {

// Verify that there are no global env vars or defaults.
if workflow.Env != nil || workflow.Defaults != nil {
return fmt.Errorf("%w", errGlobalVarsOrDefaults)
return verificationError{e: errGlobalVarsOrDefaults}
}

if workflow.Permissions != nil {
globalPerms := workflow.Permissions
// Verify that the all scope, if set, isn't write-all.
if globalPerms.All != nil && globalPerms.All.Value == "write-all" {
return fmt.Errorf("%w", errGlobalWriteAll)
return verificationError{e: errGlobalWriteAll}
}

// Verify that there are no global permissions (including id-token) set to write.
for globalPerm, val := range globalPerms.Scopes {
if val.Value.Value == "write" {
return fmt.Errorf("%w: permission for %v is set to write",
errGlobalWrite, globalPerm)
return verificationError{e: fmt.Errorf("%w: permission for %v is set to write",
errGlobalWrite, globalPerm)}
}
}
}

// Find the (first) job with a step that calls scorecard-action.
scorecardJob := findScorecardJob(workflow.Jobs)
if scorecardJob == nil {
return fmt.Errorf("%w", errScorecardJobNotFound)
return verificationError{e: errScorecardJobNotFound}
}

// Make sure other jobs don't have id-token permissions.
for _, job := range workflow.Jobs {
if job != scorecardJob && job.Permissions != nil {
idToken := job.Permissions.Scopes["id-token"]
if idToken != nil && idToken.Value.Value == "write" {
return fmt.Errorf("%w", errNonScorecardJobHasTokenWrite)
return verificationError{e: errNonScorecardJobHasTokenWrite}
}
}
}

// Verify that there is no job container or services.
if scorecardJob.Container != nil || len(scorecardJob.Services) > 0 {
return fmt.Errorf("%w", errJobHasContainerOrServices)
return verificationError{e: errJobHasContainerOrServices}
}

labels := scorecardJob.RunsOn.Labels
if len(labels) != 1 {
return fmt.Errorf("%w", errScorecardJobRunsOn)
return verificationError{e: errScorecardJobRunsOn}
}
label := labels[0].Value
if _, ok := ubuntuRunners[label]; !ok {
Expand All @@ -107,12 +123,12 @@ func verifyScorecardWorkflow(workflowContent string) error {

// Verify that there are no job env vars set.
if scorecardJob.Env != nil {
return fmt.Errorf("%w", errScorecardJobEnvVars)
return verificationError{e: errScorecardJobEnvVars}
}

// Verify that there are no job defaults set.
if scorecardJob.Defaults != nil {
return fmt.Errorf("%w", errScorecardJobDefaults)
return verificationError{e: errScorecardJobDefaults}
}

// Get steps in job.
Expand All @@ -122,7 +138,7 @@ func verifyScorecardWorkflow(workflowContent string) error {
for _, step := range steps {
stepUses := getStepUses(step)
if stepUses == nil {
return fmt.Errorf("%w", errEmptyStepUses)
return verificationError{e: errEmptyStepUses}
}
stepName := getStepName(stepUses.Value)

Expand All @@ -136,7 +152,7 @@ func verifyScorecardWorkflow(workflowContent string) error {
// Needed for e2e tests
"gcr.io/openssf/scorecard-action":
default:
return fmt.Errorf("%w: %s", errUnallowedStepName, stepName)
return verificationError{e: fmt.Errorf("%w: %s", errUnallowedStepName, stepName)}
}
}

Expand Down