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

Feature: Create an interface for downstream CIP integrations. #480

Merged
merged 2 commits into from
Jan 4, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 36 additions & 102 deletions cmd/tester/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,51 +18,35 @@ package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"knative.dev/pkg/apis"
"knative.dev/pkg/logging"
"sigs.k8s.io/release-utils/version"
"sigs.k8s.io/yaml"

"github.com/sigstore/policy-controller/pkg/apis/glob"
"github.com/sigstore/policy-controller/pkg/apis/policy/v1alpha1"
"github.com/sigstore/policy-controller/pkg/policy"
"github.com/sigstore/policy-controller/pkg/webhook"
webhookcip "github.com/sigstore/policy-controller/pkg/webhook/clusterimagepolicy"
)

var (
ns = "unused"

remoteOpts = []ociremote.Option{
ociremote.WithRemoteOptions(
remote.WithAuthFromKeychain(authn.DefaultKeychain),
),
}

ctx = logging.WithLogger(context.Background(), func() *zap.SugaredLogger {
x, _ := zap.NewDevelopmentConfig().Build()
return x.Sugar()
}())
)

type output struct {
Errors []string `json:"errors,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Result *webhook.PolicyResult `json:"result"`
Errors []string `json:"errors,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}

func main() {
Expand All @@ -83,42 +67,46 @@ func main() {
os.Exit(1)
}

var cipRaw []byte
var err error
pols := make([]policy.Source, 0, 1)

if strings.HasPrefix(*cipFilePath, "https://") || strings.HasPrefix(*cipFilePath, "http://") {
log.Printf("Fetching CIP from: %s", *cipFilePath)
resp, err := http.Get(*cipFilePath)
if err != nil {
log.Fatal(err)
}
cipRaw, err = io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatal(err)
}
pols = append(pols, policy.Source{
URL: *cipFilePath,
})
} else {
cipRaw, err = os.ReadFile(*cipFilePath)
if err != nil {
log.Fatal(err)
pols = append(pols, policy.Source{
Path: *cipFilePath,
})
}

v := policy.Verification{
NoMatchPolicy: "deny",
Policies: &pols,
}
if err := v.Validate(ctx); err != nil {
// CIP validation can return Warnings so let's just go through them
// and only exit if there are Errors.
if warnFE := err.Filter(apis.WarningLevel); warnFE != nil {
log.Printf("CIP has warnings:\n%s\n", warnFE.Error())
}
if errorFE := err.Filter(apis.ErrorLevel); errorFE != nil {
log.Fatalf("CIP is invalid: %s", errorFE.Error())
}
}

// TODO(jdolitsky): This should use v1beta1 once there exists a
// webhookcip.ConvertClusterImagePolicyV1beta1ToWebhook() method
var v1alpha1cip v1alpha1.ClusterImagePolicy
if err := yaml.UnmarshalStrict(cipRaw, &v1alpha1cip); err != nil {
ref, err := name.ParseReference(*image)
if err != nil {
log.Fatal(err)
}
v1alpha1cip.SetDefaults(ctx)

// Show what the defaults look like
defaulted, err := yaml.Marshal(v1alpha1cip)
warningStrings := []string{}
vfy, err := policy.Compile(ctx, v, func(s string, i ...interface{}) {
warningStrings = append(warningStrings, fmt.Sprintf(s, i...))
})
if err != nil {
log.Fatalf("Failed to marshal the defaulted cip: %s", err)
log.Fatal(err)
}

log.Printf("Using the following cip:\n%s", defaulted)

if *resourceFilePath != "" {
raw, err := os.ReadFile(*resourceFilePath)
if err != nil {
Expand Down Expand Up @@ -152,76 +140,22 @@ func main() {
ctx = webhook.IncludeTypeMeta(ctx, typeMeta)
}

validateErrs := v1alpha1cip.Validate(ctx)
if validateErrs != nil {
// CIP validation can return Warnings so let's just go through them
// and only exit if there are Errors.
if warnFE := validateErrs.Filter(apis.WarningLevel); warnFE != nil {
log.Printf("CIP has warnings:\n%s\n", warnFE.Error())
}
if errorFE := validateErrs.Filter(apis.ErrorLevel); errorFE != nil {
log.Fatalf("CIP is invalid: %s", errorFE.Error())
}
}
cip := webhookcip.ConvertClusterImagePolicyV1alpha1ToWebhook(&v1alpha1cip)

// We have to marshal/unmarshal the CIP since that handles converting
// inlined Data into PublicKey objects that validator uses.
webhookCip, err := json.Marshal(cip)
if err != nil {
log.Fatalf("Failed to marshal the webhook cip: %s", err)
}
if err := json.Unmarshal(webhookCip, &cip); err != nil {
log.Fatalf("Failed to unmarshal the webhook CIP: %s", err)
}
ref, err := name.ParseReference(*image)
if err != nil {
log.Fatal(err)
}

matches := false
for _, pattern := range cip.Images {
if pattern.Glob != "" {
if matched, err := glob.Match(pattern.Glob, *image); err != nil {
log.Fatalf("Failed to match glob: %s", err)
} else if matched {
log.Printf("image matches glob %q", pattern.Glob)
matches = true
}
}
}
if !matches {
log.Fatalf("Image does not match any of the provided globs")
}

result, errs := webhook.ValidatePolicy(ctx, ns, ref, *cip, authn.DefaultKeychain, remoteOpts...)
errStrings := []string{}
warningStrings := []string{}
for _, err := range errs {
var fe *apis.FieldError
if errors.As(err, &fe) {
if warnFE := fe.Filter(apis.WarningLevel); warnFE != nil {
warningStrings = append(warningStrings, strings.Trim(warnFE.Error(), "\n"))
}
if errorFE := fe.Filter(apis.ErrorLevel); errorFE != nil {
errStrings = append(errStrings, strings.Trim(errorFE.Error(), "\n"))
}
} else {
errStrings = append(errStrings, strings.Trim(err.Error(), "\n"))
}
if err := vfy.Verify(ctx, ref, authn.DefaultKeychain); err != nil {
errStrings = append(errStrings, strings.Trim(err.Error(), "\n"))
}

var o []byte
o, err = json.Marshal(&output{
Errors: errStrings,
Warnings: warningStrings,
Result: result,
})
if err != nil {
log.Fatal(err)
}

fmt.Println(string(o))
if len(errs) > 0 {
if len(errStrings) > 0 {
os.Exit(1)
}
}
105 changes: 105 additions & 0 deletions pkg/policy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Integrating Policy Verification

The goal of this package is to make it easy for downstream tools to incorporate
the verification capabilities of `ClusterImagePolicy` in other contexts where
OCI artifacts are consumed.

The most straightforward example of this is to enable OCI build tooling to
incorporate policies over the base images on top of which an application image
is built (e.g. `ko`, `kaniko`). However, this can be used by other tooling
that stores artifacts in OCI registries to verify those as well, examples of
this could include the way Buildpacks v3 and Crossplane store elements in OCI
registries.

## Configuration

Verification is configured via `policy.Verification`:

```golang
type Verification struct {
// NoMatchPolicy specifies the behavior when a base image doesn't match any
// of the listed policies. It allows the values: allow, deny, and warn.
NoMatchPolicy string `yaml:"no-match-policy,omitempty"`

// Policies specifies a collection of policies to use to cover the base
// images used as part of evaluation. See "policy" below for usage.
// Policies can be nil so that we can distinguish between an explicitly
// specified empty list and when policies is unspecified.
Policies *[]Source `yaml:"policies,omitempty"`
}
```

`NoMatchPolicy` controls the behavior when an image reference is passed that
does not match any of the configured policies.

`Policies` can be specified via three possible sources:

```golang
// Source contains a set of options for specifying policies. Exactly
// one of the fields may be specified for each Source entry.
type Source struct {
// Data is a collection of one or more ClusterImagePolicy resources.
Data string `yaml:"data,omitempty"`

// Path is a path to a file containing one or more ClusterImagePolicy
// resources.
// TODO(mattmoor): Make this support taking a directory similar to kubectl.
// TODO(mattmoor): How do we want to handle something like -R? Perhaps we
// don't and encourage folks to list each directory individually?
Path string `yaml:"path,omitempty"`

// URL links to a file containing one or more ClusterImagePolicy resources.
URL string `yaml:"url,omitempty"`
}
```

### With `spf13/viper`

Many tools leverage `spf13/viper` for configuration, and `policy.Verification`
may be used in conjunction with viper via:

```golang
vfy := policy.Verification{}
if err := v.UnmarshalKey("verification", &vfy); err != nil { ... }
```

This allows a section of the viper config:

```yaml
verification:
noMatchPolicy: deny
policies:
- data: ... # Inline policies
- url: ... # URL to policies
...
```

## Compilation

The `policy.Verification` can be compiled into a `policy.Verifier` using
`policy.Compile`, which also takes a `context.Context` and a function that
controls how warnings are surfaced:

```golang
verifier, err := policy.Compile(ctx, verification,
func(s string, i ...interface{}) {
// Handle warnings your own way!
})
if err != nil { ... }
```

The compilation process will surface compilation warnings via the supplied
function and return any errors resolving or compiling the policies immediately.

## Verification

With a compiled `policy.Verifier` many image references can be verified against
the compiled policies by invoking `Verify`:
```golang
// Verifier is the interface for checking that a given image digest satisfies
// the policies backing this interface.
type Verifier interface {
// Verify checks that the provided reference satisfies the backing policies.
Verify(context.Context, name.Reference, authn.Keychain) error
}
```
Loading