diff --git a/Makefile b/Makefile index 54b3c363c83..d1dcddeb423 100644 --- a/Makefile +++ b/Makefile @@ -243,6 +243,12 @@ build-attestor: ## Runs go build on scorecard attestor # Run go build on scorecard attestor cd attestor/; CGO_ENABLED=0 go build -trimpath -a -tags netgo -ldflags '$(LDFLAGS)' -o scorecard-attestor +build-attestor-docker: ## Build scorecard-attestor Docker image +build-attestor-docker: + DOCKER_BUILDKIT=1 docker build . --file attestor/Dockerfile \ + --tag scorecard-attestor:latest \ + --tag scorecard-atttestor:$(GIT_HASH) + TOKEN_SERVER_DEPS = $(shell find clients/githubrepo/roundtripper/tokens/ -iname "*.go") build-github-server: ## Build GitHub token server build-github-server: clients/githubrepo/roundtripper/tokens/server/github-auth-server diff --git a/attestor/Dockerfile b/attestor/Dockerfile new file mode 100644 index 00000000000..8f4a0feed95 --- /dev/null +++ b/attestor/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2022 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. + +FROM golang@sha256:ea3d912d500b1ae0a691b2e53eb8a6345b579d42d7e6a64acca83d274b949740 AS base +WORKDIR /src/scorecard +COPY . ./ + +FROM base AS build +ARG TARGETOS +ARG TARGETARCH +RUN make build-attestor + +FROM gcr.io/google-appengine/debian11@sha256:fed7dd5b2c4bbfb70bd26a277cdaff98dced71f113632ccd5451dcc013fce0a4 +COPY --from=build /src/scorecard/attestor / +ENTRYPOINT [ "/scorecard-attestor" ] + diff --git a/attestor/README.md b/attestor/README.md new file mode 100644 index 00000000000..253fd6ca7be --- /dev/null +++ b/attestor/README.md @@ -0,0 +1,69 @@ +# Scorecard Attestor + +## What is scorecard-attestor? + +scorecard-attestor is a tool that runs scorecard on a software source repo, and based on certain policies about those results, produces a Google Cloud binary authorization attestation. + +scorecard-attestor helps users secure their software deployment systems by ensuring the code that they deploy passes certain criteria. + +## Building and using scorecard-attestor + +scorecard-attestor can be built as a standalone binary from source using `make build-attestor`, or with Docker, using `make build-attestor-docker`. scorecard-attestor is intended to be used as part of a Google Cloud Build pipeline, and inherits environment variables based on [build substitutions](https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values). + +Unless there's an internal error, scorecard-attestor will always return a successful status code, but will only produce a binary authorization attestation if the policy check passes. + +## Configuring policies for scorecard-attestor + +Policies for scorecard attestor can be passed through the CLI using the `--policy` flag. Examples of policies can be seen in [attestor/policy/testdata](/attestor/policy/testdata). + +### Policy schema + +Policies follow the following schema: + +```yaml +--- +type: "//rec" +optional: + preventBinaryArtifacts: "//bool" + allowedBinaryArtifacts: + type: "//arr" + contents: "//str" # Accepts glob-based filepaths as strings here + ensureNoVulnerabilities: "//bool" + ensureDependenciesPinned: "//bool" + allowedUnpinnedDependencies: + type: "//arr" + contents: + type: "//rec" + optional: + packagename: "//str" + filepath: "//str" + version: "//str" + ensureCodeReviewed: "//bool" + codeReviewRequirements: + type: "//rec" + optional: + requiredApprovers: + type: "//arr" + contents: "//str" + minReviewers: "//int" +``` + +### Missing parameters + +Policies that are left blank will be ignored. Policies that allow users additional configuration options will be given default parameters as listed below. + +* `PreventBinaryArtifacts`: If not specified, `AllowedBinaryArtifacts` will be empty, i.e. no binary artifacts will be allowed +* `PreventUnpinnedDependencies`: If not specified, `AllowedUnpinnedDependencies` will be empty, i.e. no unpinned dependencies will be allowed +* `RequireCodeReviewed`: If not specified, `CodeReviewRequirements` will require at least one reviewer on all changesets. + +## Sample + +Examples of how to use scorecard-attestor with binary authorization in your project can be found in these two repos: + +* [scorecard-binauthz-test-good](https://github.com/ossf-tests/scorecard-binauthz-test-good) +* [scorecard-binauthz-test-bad](https://github.com/ossf-tests/scorecard-binauthz-test-bad) + +Sample code comes with: + +* `cloudbuild.yaml` to build the application and run scorecard-attestor +* Terraform files to set up the binary authorization environment, including KMS and IAM. diff --git a/attestor/command/check.go b/attestor/command/check.go index a4ac9086bee..7185c4594b6 100644 --- a/attestor/command/check.go +++ b/attestor/command/check.go @@ -26,23 +26,34 @@ import ( "github.com/ossf/scorecard/v4/pkg" ) -func runCheck() error { +type EmptyParameterError struct { + Param string +} + +func (ep EmptyParameterError) Error() string { + return fmt.Sprintf("param %s is empty", ep.Param) +} + +func runCheck() (policy.PolicyResult, error) { ctx := context.Background() logger := sclog.NewLogger(sclog.DefaultLevel) // Read the Binauthz attestation policy if policyPath == "" { - return fmt.Errorf("policy path is empty") + return policy.Fail, EmptyParameterError{Param: "policy"} } + + var attestationPolicy *policy.AttestationPolicy + attestationPolicy, err := policy.ParseAttestationPolicyFromFile(policyPath) if err != nil { - return fmt.Errorf("fail to load scorecard attestation policy: %v", err) + return policy.Fail, fmt.Errorf("fail to load scorecard attestation policy: %w", err) } if repoURL == "" { buildRepo := os.Getenv("REPO_NAME") if buildRepo == "" { - return fmt.Errorf("repoURL not specified") + return policy.Fail, EmptyParameterError{Param: "repoURL"} } repoURL = buildRepo logger.Info(fmt.Sprintf("Found repo URL %s Cloud Build environment", repoURL)) @@ -54,14 +65,18 @@ func runCheck() error { buildSHA := os.Getenv("COMMIT_SHA") if buildSHA == "" { logger.Info("commit not specified, running on HEAD") + commitSHA = "HEAD" } else { commitSHA = buildSHA - logger.Info(fmt.Sprintf("Found revision %s Cloud Build environment", commitSHA)) + logger.Info(fmt.Sprintf("Found revision %s from GCB build environment", commitSHA)) } } repo, repoClient, ossFuzzRepoClient, ciiClient, vulnsClient, err := checker.GetClients( ctx, repoURL, "", logger) + if err != nil { + return policy.Fail, fmt.Errorf("couldn't set up clients: %w", err) + } requiredChecks := attestationPolicy.GetRequiredChecksForPolicy() @@ -89,16 +104,17 @@ func runCheck() error { vulnsClient, ) if err != nil { - return fmt.Errorf("RunScorecards: %w", err) + return policy.Fail, fmt.Errorf("RunScorecards: %w", err) } result, err := attestationPolicy.EvaluateResults(&repoResult.RawResults) if err != nil { - return fmt.Errorf("error when evaluating image %q against policy", image) + return policy.Fail, fmt.Errorf("error when evaluating image %q against policy: %w", image, err) } if result != policy.Pass { - return fmt.Errorf("image failed policy check %s:", image) + logger.Info("image failed scorecard attestation policy check") + } else { + logger.Info("image passed scorecard attestation policy check") } - logger.Info("Policy check passed") - return nil + return result, nil } diff --git a/attestor/command/cli.go b/attestor/command/cli.go index fbeb0a3fe2f..05d04abb4e5 100644 --- a/attestor/command/cli.go +++ b/attestor/command/cli.go @@ -74,21 +74,31 @@ var RootCmd = &cobra.Command{ var checkAndSignCmd = &cobra.Command{ Use: "attest", - Short: "Run scorecard and sign a container image according to policy", + Short: "Run scorecard and sign a container image if attestation policy check passes", RunE: func(cmd *cobra.Command, args []string) error { - if err := runCheck(); err != nil { + passed, err := runCheck() + + if err != nil { return err } - return runSign() + + if passed { + return runSign() + } + + return nil }, + SilenceUsage: true, } var checkCmd = &cobra.Command{ Use: "verify", Short: "Run scorecard and check an image against a policy", RunE: func(cmd *cobra.Command, args []string) error { - return runCheck() + _, err := runCheck() + return err }, + SilenceUsage: true, } func init() { diff --git a/attestor/command/sign.go b/attestor/command/sign.go index 4edf7b7b973..2fcded3b244 100644 --- a/attestor/command/sign.go +++ b/attestor/command/sign.go @@ -17,6 +17,7 @@ package command import ( "fmt" "io/ioutil" + "strings" "github.com/grafeas/kritis/pkg/attestlib" "github.com/grafeas/kritis/pkg/kritis/metadata/containeranalysis" @@ -47,6 +48,7 @@ func runSign() error { if kmsDigestAlg == "" { return fmt.Errorf("kms_digest_alg is unspecified, must be one of SHA256|SHA384|SHA512, and the same as specified by the key version's algorithm") } + kmsDigestAlg = strings.ToUpper(kmsDigestAlg) cSigner, err = signer.NewCloudKmsSigner(kmsKeyName, signer.DigestAlgorithm(kmsDigestAlg)) if err != nil { return fmt.Errorf("creating kms signer failed: %v\n", err) @@ -81,9 +83,9 @@ func runSign() error { // Parse attestation project if attestationProject == "" { attestationProject = util.GetProjectFromContainerImage(image) - logger.Info(fmt.Sprintf("Using image project as attestation project: %s\n", attestationProject)) + logger.Info(fmt.Sprintf("Using image project as attestation project: %s", attestationProject)) } else { - logger.Info(fmt.Sprintf("Using specified attestation project: %s\n", attestationProject)) + logger.Info(fmt.Sprintf("Using specified attestation project: %s", attestationProject)) } // Check note name diff --git a/attestor/e2e/command_test.go b/attestor/e2e/command_test.go index 150b6301c86..1be27a2a3d8 100644 --- a/attestor/e2e/command_test.go +++ b/attestor/e2e/command_test.go @@ -18,8 +18,9 @@ import ( "strings" "testing" - "github.com/ossf/scorecard-attestor/command" "github.com/spf13/cobra" + + "github.com/ossf/scorecard-attestor/command" ) func execute(t *testing.T, c *cobra.Command, args ...string) (string, error) { @@ -35,6 +36,7 @@ func execute(t *testing.T, c *cobra.Command, args ...string) (string, error) { } func TestRootCmd(t *testing.T) { + t.Parallel() tt := []struct { name string args []string @@ -51,7 +53,6 @@ func TestRootCmd(t *testing.T) { for _, tc := range tt { _, err := execute(t, command.RootCmd, tc.args...) - if err != nil { t.Fatalf("%s: %s", tc.name, err) } diff --git a/attestor/policy/attestation_policy.go b/attestor/policy/attestation_policy.go index d946b154aaa..28044627cbe 100644 --- a/attestor/policy/attestation_policy.go +++ b/attestor/policy/attestation_policy.go @@ -98,7 +98,6 @@ func (ap *AttestationPolicy) EvaluateResults(raw *checker.RawResults) (PolicyRes dl := checker.NewLogger() if ap.PreventBinaryArtifacts { checkResult, err := CheckPreventBinaryArtifacts(ap.AllowedBinaryArtifacts, raw, dl) - if !checkResult || err != nil { return checkResult, err }