Skip to content

Commit

Permalink
🌱 attestor: Dockerize + small improvements for Cloud Build usage (oss…
Browse files Browse the repository at this point in the history
…f#2456)

* Dockerize

* Add cloudbuild.yaml
* Improve logging

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Add README.md

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Address PR comments

* debian10 -> 11
* CLI
* Remove logging statements
* Dockerfile

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

Signed-off-by: Raghav Kaul <raghavkaul@google.com>
  • Loading branch information
raghavkaul committed Feb 9, 2023
1 parent 0346745 commit 0568240
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 19 deletions.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions attestor/Dockerfile
Original file line number Diff line number Diff line change
@@ -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" ]

69 changes: 69 additions & 0 deletions attestor/README.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 26 additions & 10 deletions attestor/command/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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()

Expand Down Expand Up @@ -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
}
18 changes: 14 additions & 4 deletions attestor/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
6 changes: 4 additions & 2 deletions attestor/command/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions attestor/e2e/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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)
}
Expand Down
1 change: 0 additions & 1 deletion attestor/policy/attestation_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 0568240

Please sign in to comment.