Skip to content

Commit

Permalink
Spin verification errors into custom type.
Browse files Browse the repository at this point in the history
This lets us better control which errors we surface as a 400 vs hide as 500.

Signed-off-by: Spencer Schrock <sschrock@google.com>
  • Loading branch information
spencerschrock committed Jun 5, 2023
1 parent d6ec35b commit 4428c6e
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 25 deletions.
25 changes: 12 additions & 13 deletions app/server/post_results.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@ import (
const (
// OID source: https://github.com/sigstore/fulcio/blob/96ef49cc7662912ba37d46f738757e8d8d5b5355/docs/oid-info.md#L33
// TODO: retrieve these by name.
fulcioRepoRefKey = "1.3.6.1.4.1.57264.1.6"
fulcioRepoPathKey = "1.3.6.1.4.1.57264.1.5"
fulcioRepoSHAKey = "1.3.6.1.4.1.57264.1.3"
resultsBucket = "gs://ossf-scorecard-results"
resultsFile = "results.json"
workflowRestrictionLink = "https://github.com/ossf/scorecard-action#workflow-restrictions"
fulcioRepoRefKey = "1.3.6.1.4.1.57264.1.6"
fulcioRepoPathKey = "1.3.6.1.4.1.57264.1.5"
fulcioRepoSHAKey = "1.3.6.1.4.1.57264.1.3"
resultsBucket = "gs://ossf-scorecard-results"
resultsFile = "results.json"
)

var (
Expand All @@ -63,7 +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")
errWorkflowVerification = errors.New("workflow verification failed")
errNotDefaultBranch = errors.New("branch of cert isn't the repo's default branch")
)

type certInfo struct {
Expand Down Expand Up @@ -110,10 +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, errWorkflowVerification) {
var vErr verificationError
if errors.As(err, &vErr) {
return results.NewPostResultBadRequest().WithPayload(&models.Error{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("%v, see %s for details.", err, workflowRestrictionLink),
Message: vErr.Error(),
})
}
log.Println(err)
Expand All @@ -137,12 +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 errMismatchedCertAndRequest
return verificationError{e: errMismatchedCertAndRequest}
}

if err := getAndVerifyWorkflowContent(ctx, scorecardResult, info); err != nil {
// TODO(go 1.20) wrap multiple errors https://go.dev/doc/go1.20#errors
return fmt.Errorf("%w: %v", errWorkflowVerification, err)
return fmt.Errorf("workflow verification failed: %w", err)
}

// Save scorecard results (results.json, score.txt) to GCS
Expand Down Expand Up @@ -202,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 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 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 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 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 errNonScorecardJobHasTokenWrite
return verificationError{e: errNonScorecardJobHasTokenWrite}
}
}
}

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

labels := scorecardJob.RunsOn.Labels
if len(labels) != 1 {
return 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 errScorecardJobEnvVars
return verificationError{e: errScorecardJobEnvVars}
}

// Verify that there are no job defaults set.
if scorecardJob.Defaults != nil {
return 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 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

0 comments on commit 4428c6e

Please sign in to comment.