feat(misconf): Fetch policies from OCI registry (#3015)

Signed-off-by: Simar <simar@linux.com>
This commit is contained in:
simar7
2023-01-15 03:37:04 -08:00
committed by GitHub
parent 682351a131
commit a1d4427c8b
12 changed files with 701 additions and 35 deletions

View File

@@ -21,8 +21,18 @@ Helm Chart scanning will resolve the chart to Kubernetes manifests then run the
Ansible scanning is coming soon.
[rego]: https://www.openpolicyagent.org/docs/latest/policy-language
## Policy Distribution
defsec policies are distributed as an OPA bundle on [GitHub Container Registry][ghcr] (GHCR).
When misconfiguration detection is enabled, Trivy pulls the OPA bundle from GHCR as an OCI artifact and stores it in the cache.
Those policies are then loaded into Trivy OPA engine and used for detecting misconfigurations.
If Trivy is unable to pull down newer policies, it will use the embedded set of policies as a fallback. This is also the case in air-gap environments where `--skip-policy-update` might be passed.
## Update Interval
Trivy checks for updates to OPA bundle on GHCR every 24 hours and pulls it if there are any updates.
[rego]: https://www.openpolicyagent.org/docs/latest/policy-language/
[defsec]: https://github.com/aquasecurity/defsec
[kubernetes]: https://github.com/aquasecurity/defsec/tree/master/internal/rules/policies/kubernetes
[docker]: https://github.com/aquasecurity/defsec/tree/master/internal/rules/policies/docker
[rbac]: https://github.com/aquasecurity/defsec/tree/master/internal/rules/policies/rbac
[kubernetes]: https://github.com/aquasecurity/defsec/tree/master/internal/rules/kubernetes
[kubernetes]: https://github.com/aquasecurity/defsec/tree/master/internal/rules/rbac
[docker]: https://github.com/aquasecurity/defsec/tree/master/internal/rules/docker
[ghcr]: https://github.com/aquasecurity/defsec/pkgs/container/defsec

View File

@@ -8,11 +8,10 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Run(t *testing.T) {
@@ -29,13 +28,16 @@ func Test_Run(t *testing.T) {
}{
{
name: "fail without region",
options: flag.Options{},
options: flag.Options{
RegoOptions: flag.RegoOptions{SkipPolicyUpdate: true},
},
want: "",
expectErr: true,
},
{
name: "fail without creds",
options: flag.Options{
RegoOptions: flag.RegoOptions{SkipPolicyUpdate: true},
AWSOptions: flag.AWSOptions{
Region: "us-east-1",
},
@@ -46,6 +48,7 @@ func Test_Run(t *testing.T) {
{
name: "try to call aws if cache is expired",
options: flag.Options{
RegoOptions: flag.RegoOptions{SkipPolicyUpdate: true},
AWSOptions: flag.AWSOptions{
Region: "us-east-1",
Services: []string{"s3"},
@@ -61,6 +64,7 @@ func Test_Run(t *testing.T) {
{
name: "succeed with cached infra",
options: flag.Options{
RegoOptions: flag.RegoOptions{SkipPolicyUpdate: true},
AWSOptions: flag.AWSOptions{
Region: "us-east-1",
Services: []string{"s3"},

View File

@@ -11,6 +11,7 @@ import (
"github.com/aquasecurity/defsec/pkg/scanners/options"
"github.com/aquasecurity/defsec/pkg/state"
"github.com/aquasecurity/trivy/pkg/cloud/aws/cache"
"github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
)
@@ -60,12 +61,22 @@ func (s *AWSScanner) Scan(ctx context.Context, option flag.Options) (scan.Result
)
}
if len(option.RegoOptions.PolicyPaths) > 0 {
scannerOpts = append(
scannerOpts,
options.ScannerWithPolicyDirs(option.RegoOptions.PolicyPaths...),
)
var policyPaths []string
var downloadedPolicyPaths []string
var err error
downloadedPolicyPaths, err = operation.InitBuiltinPolicies(context.Background(), option.CacheDir, option.Quiet, option.SkipPolicyUpdate)
if err != nil {
if !option.SkipPolicyUpdate {
log.Logger.Errorf("Falling back to embedded policies: %s", err)
}
} else {
log.Logger.Debug("Policies successfully loaded from disk")
policyPaths = append(policyPaths, downloadedPolicyPaths...)
scannerOpts = append(scannerOpts,
options.ScannerWithEmbeddedPolicies(false))
}
policyPaths = append(policyPaths, option.RegoOptions.PolicyPaths...)
scannerOpts = append(scannerOpts, options.ScannerWithPolicyDirs(policyPaths...))
if len(option.RegoOptions.PolicyNamespaces) > 0 {
scannerOpts = append(

View File

@@ -492,6 +492,18 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi
log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType)
}
var downloadedPolicyPaths []string
var disableEmbedded bool
downloadedPolicyPaths, err := operation.InitBuiltinPolicies(context.Background(), opts.CacheDir, opts.Quiet, opts.SkipPolicyUpdate)
if err != nil {
if !opts.SkipPolicyUpdate {
log.Logger.Errorf("Falling back to embedded policies: %s", err)
}
} else {
log.Logger.Debug("Policies successfully loaded from disk")
disableEmbedded = true
}
// ScannerOption is filled only when config scanning is enabled.
var configScannerOptions config.ScannerOption
if slices.Contains(opts.SecurityChecks, types.SecurityCheckConfig) {
@@ -499,13 +511,14 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi
configScannerOptions = config.ScannerOption{
Trace: opts.Trace,
Namespaces: append(opts.PolicyNamespaces, defaultPolicyNamespaces...),
PolicyPaths: opts.PolicyPaths,
PolicyPaths: append(opts.PolicyPaths, downloadedPolicyPaths...),
DataPaths: opts.DataPaths,
HelmValues: opts.HelmValues,
HelmValueFiles: opts.HelmValueFiles,
HelmFileValues: opts.HelmFileValues,
HelmStringValues: opts.HelmStringValues,
TerraformTFVars: opts.TerraformTFVars,
DisableEmbeddedPolicies: disableEmbedded,
}
}

View File

@@ -6,6 +6,8 @@ import (
"os"
"strings"
"github.com/aquasecurity/trivy/pkg/policy"
"github.com/samber/lo"
"github.com/aquasecurity/trivy/pkg/flag"
@@ -135,3 +137,38 @@ func showDBInfo(cacheDir string) error {
meta.Version, meta.UpdatedAt, meta.NextUpdate, meta.DownloadedAt)
return nil
}
// InitBuiltinPolicies downloads the built-in policies and loads them
func InitBuiltinPolicies(ctx context.Context, cacheDir string, quiet, skipUpdate bool) ([]string, error) {
client, err := policy.NewClient(cacheDir, quiet)
if err != nil {
return nil, xerrors.Errorf("policy client error: %w", err)
}
needsUpdate := false
if !skipUpdate {
needsUpdate, err = client.NeedsUpdate()
if err != nil {
return nil, xerrors.Errorf("unable to check if built-in policies need to be updated: %w", err)
}
}
if needsUpdate {
log.Logger.Info("Need to update the built-in policies")
log.Logger.Info("Downloading the built-in policies...")
if err = client.DownloadBuiltinPolicies(ctx); err != nil {
return nil, xerrors.Errorf("failed to download built-in policies: %w", err)
}
}
policyPaths, err := client.LoadBuiltinPolicies()
if err != nil {
if skipUpdate {
msg := "No downloadable policies were loaded as --skip-policy-update is enabled"
log.Logger.Info(msg)
return nil, xerrors.Errorf(msg)
}
return nil, xerrors.Errorf("policy load error: %w", err)
}
return policyPaths, nil
}

View File

@@ -1,9 +1,5 @@
package flag
import (
"github.com/aquasecurity/trivy/pkg/log"
)
// e.g. config yaml:
//
// rego:
@@ -15,8 +11,7 @@ var (
Name: "skip-policy-update",
ConfigName: "rego.skip-policy-update",
Value: false,
Usage: "deprecated",
Deprecated: true,
Usage: "skip fetching rego policy updates",
}
TraceFlag = Flag{
Name: "trace",
@@ -46,7 +41,7 @@ var (
// RegoFlagGroup composes common printer flag structs used for commands providing misconfinguration scanning.
type RegoFlagGroup struct {
SkipPolicyUpdate *Flag // deprecated
SkipPolicyUpdate *Flag
Trace *Flag
PolicyPaths *Flag
DataPaths *Flag
@@ -54,7 +49,7 @@ type RegoFlagGroup struct {
}
type RegoOptions struct {
SkipPolicyUpdate bool // deprecated
SkipPolicyUpdate bool
Trace bool
PolicyPaths []string
DataPaths []string
@@ -86,10 +81,6 @@ func (f *RegoFlagGroup) Flags() []*Flag {
}
func (f *RegoFlagGroup) ToOptions() (RegoOptions, error) {
skipPolicyUpdateFlag := getBool(f.SkipPolicyUpdate)
if skipPolicyUpdateFlag {
log.Logger.Warn("'--skip-policy-update' is no longer necessary as the built-in policies are embedded into the binary")
}
return RegoOptions{
SkipPolicyUpdate: getBool(f.SkipPolicyUpdate),
Trace: getBool(f.Trace),

216
pkg/policy/policy.go Normal file
View File

@@ -0,0 +1,216 @@
package policy
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/open-policy-agent/opa/bundle"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/oci"
"golang.org/x/xerrors"
"k8s.io/utils/clock"
)
const (
bundleVersion = 0 // Latest released MAJOR version for defsec
bundleRepository = "ghcr.io/aquasecurity/defsec"
policyMediaType = "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip"
updateInterval = 24 * time.Hour
)
type options struct {
artifact *oci.Artifact
clock clock.Clock
}
// WithOCIArtifact takes an OCI artifact
func WithOCIArtifact(art *oci.Artifact) Option {
return func(opts *options) {
opts.artifact = art
}
}
// WithClock takes a clock
func WithClock(clock clock.Clock) Option {
return func(opts *options) {
opts.clock = clock
}
}
// Option is a functional option
type Option func(*options)
// Client implements policy operations
type Client struct {
*options
policyDir string
quiet bool
insecure bool
}
// Metadata holds default policy metadata
type Metadata struct {
Digest string
DownloadedAt time.Time
}
// NewClient is the factory method for policy client
func NewClient(cacheDir string, quiet bool, opts ...Option) (*Client, error) {
o := &options{
clock: clock.RealClock{},
}
for _, opt := range opts {
opt(o)
}
return &Client{
options: o,
policyDir: filepath.Join(cacheDir, "policy"),
quiet: quiet,
}, nil
}
func (c *Client) populateOCIArtifact() error {
if c.artifact == nil {
repo := fmt.Sprintf("%s:%d", bundleRepository, bundleVersion)
art, err := oci.NewArtifact(repo, policyMediaType, c.quiet, c.insecure)
if err != nil {
return xerrors.Errorf("OCI artifact error: %w", err)
}
c.artifact = art
}
return nil
}
// DownloadBuiltinPolicies download default policies from GitHub Pages
func (c *Client) DownloadBuiltinPolicies(ctx context.Context) error {
if err := c.populateOCIArtifact(); err != nil {
return xerrors.Errorf("OPA bundle error: %w", err)
}
dst := c.contentDir()
if err := c.artifact.Download(ctx, dst); err != nil {
return xerrors.Errorf("download error: %w", err)
}
digest, err := c.artifact.Digest()
if err != nil {
return xerrors.Errorf("digest error: %w", err)
}
log.Logger.Debugf("Digest of the built-in policies: %s", digest)
// Update metadata.json with the new digest and the current date
if err = c.updateMetadata(digest, c.clock.Now()); err != nil {
return xerrors.Errorf("unable to update the policy metadata: %w", err)
}
return nil
}
// LoadBuiltinPolicies loads default policies
func (c *Client) LoadBuiltinPolicies() ([]string, error) {
f, err := os.Open(c.manifestPath())
if err != nil {
return nil, xerrors.Errorf("manifest file open error (%s): %w", c.manifestPath(), err)
}
defer f.Close()
var manifest bundle.Manifest
if err = json.NewDecoder(f).Decode(&manifest); err != nil {
return nil, xerrors.Errorf("json decode error (%s): %w", c.manifestPath(), err)
}
// If the "roots" field is not included in the manifest it defaults to [""]
// which means that ALL data and policy must come from the bundle.
if manifest.Roots == nil || len(*manifest.Roots) == 0 {
return []string{c.contentDir()}, nil
}
var policyPaths []string
for _, root := range *manifest.Roots {
policyPaths = append(policyPaths, filepath.Join(c.contentDir(), root))
}
return policyPaths, nil
}
// NeedsUpdate returns if the default policy should be updated
func (c *Client) NeedsUpdate() (bool, error) {
f, err := os.Open(c.metadataPath())
if err != nil {
log.Logger.Debugf("Failed to open the policy metadata: %s", err)
return true, nil
}
defer f.Close()
var meta Metadata
if err = json.NewDecoder(f).Decode(&meta); err != nil {
log.Logger.Warnf("Policy metadata decode error: %s", err)
return true, nil
}
// No need to update if it's been within a day since the last update.
if c.clock.Now().Before(meta.DownloadedAt.Add(updateInterval)) {
return false, nil
}
if err = c.populateOCIArtifact(); err != nil {
return false, xerrors.Errorf("OPA bundle error: %w", err)
}
digest, err := c.artifact.Digest()
if err != nil {
return false, xerrors.Errorf("digest error: %w", err)
}
if meta.Digest != digest {
return true, nil
}
// Update DownloadedAt with the current time.
// Otherwise, if there are no updates in the remote registry,
// the digest will be fetched every time even after this.
if err = c.updateMetadata(meta.Digest, time.Now()); err != nil {
return false, xerrors.Errorf("unable to update the policy metadata: %w", err)
}
return false, nil
}
func (c *Client) contentDir() string {
return filepath.Join(c.policyDir, "content")
}
func (c *Client) metadataPath() string {
return filepath.Join(c.policyDir, "metadata.json")
}
func (c *Client) manifestPath() string {
return filepath.Join(c.contentDir(), bundle.ManifestExt)
}
func (c *Client) updateMetadata(digest string, now time.Time) error {
f, err := os.Create(c.metadataPath())
if err != nil {
return xerrors.Errorf("failed to open a policy manifest: %w", err)
}
defer f.Close()
meta := Metadata{
Digest: digest,
DownloadedAt: now,
}
if err = json.NewEncoder(f).Encode(meta); err != nil {
return xerrors.Errorf("json encode error: %w", err)
}
return nil
}

374
pkg/policy/policy_test.go Normal file
View File

@@ -0,0 +1,374 @@
package policy_test
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"time"
v1 "github.com/google/go-containerregistry/pkg/v1"
fakei "github.com/google/go-containerregistry/pkg/v1/fake"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/utils/clock"
fake "k8s.io/utils/clock/testing"
"github.com/aquasecurity/trivy/pkg/oci"
"github.com/aquasecurity/trivy/pkg/policy"
)
type fakeLayer struct {
v1.Layer
}
func (f fakeLayer) MediaType() (types.MediaType, error) {
return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil
}
func newFakeLayer(t *testing.T) v1.Layer {
layer, err := tarball.LayerFromFile("testdata/bundle.tar.gz")
require.NoError(t, err)
require.NotNil(t, layer)
return fakeLayer{layer}
}
type brokenLayer struct {
v1.Layer
}
func (b brokenLayer) MediaType() (types.MediaType, error) {
return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil
}
func (b brokenLayer) Compressed() (io.ReadCloser, error) {
return nil, fmt.Errorf("compressed error")
}
func newBrokenLayer(t *testing.T) v1.Layer {
layer, err := tarball.LayerFromFile("testdata/bundle.tar.gz")
require.NoError(t, err)
return brokenLayer{layer}
}
func TestClient_LoadBuiltinPolicies(t *testing.T) {
tests := []struct {
name string
cacheDir string
want []string
wantErr string
}{
{
name: "happy path",
cacheDir: "testdata/happy",
want: []string{
filepath.Join("testdata/happy/policy/content/kubernetes"),
filepath.Join("testdata/happy/policy/content/docker"),
},
},
{
name: "empty roots",
cacheDir: "testdata/empty",
want: []string{
filepath.Join("testdata/empty/policy/content"),
},
},
{
name: "broken manifest",
cacheDir: "testdata/broken",
want: []string{},
wantErr: "json decode error",
},
{
name: "no such file",
cacheDir: "testdata/unknown",
want: []string{},
wantErr: "manifest file open error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock image
img := new(fakei.FakeImage)
img.LayersReturns([]v1.Layer{newFakeLayer(t)}, nil)
img.ManifestReturns(&v1.Manifest{
Layers: []v1.Descriptor{
{
MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
Size: 100,
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8"},
Annotations: map[string]string{
"org.opencontainers.image.title": "bundle.tar.gz",
},
},
},
}, nil)
// Mock OCI artifact
mediaType := "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip"
art, err := oci.NewArtifact("repo", mediaType, true, true, oci.WithImage(img))
require.NoError(t, err)
c, err := policy.NewClient(tt.cacheDir, true, policy.WithOCIArtifact(art))
require.NoError(t, err)
got, err := c.LoadBuiltinPolicies()
if tt.wantErr != "" {
require.NotNil(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestClient_NeedsUpdate(t *testing.T) {
type digestReturns struct {
h v1.Hash
err error
}
tests := []struct {
name string
clock clock.Clock
digestReturns digestReturns
metadata interface{}
want bool
wantErr bool
}{
{
name: "recent download",
clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
digestReturns: digestReturns{
h: v1.Hash{Algorithm: "sha256", Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d"},
},
metadata: policy.Metadata{
Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`,
DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
},
want: false,
},
{
name: "same digest",
clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)),
digestReturns: digestReturns{
h: v1.Hash{Algorithm: "sha256", Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d"},
},
metadata: policy.Metadata{
Digest: `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`,
DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
},
want: false,
},
{
name: "different digest",
clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)),
digestReturns: digestReturns{
h: v1.Hash{Algorithm: "sha256", Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d"},
},
metadata: policy.Metadata{
Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`,
DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
},
want: true,
},
{
name: "sad: Digest returns an error",
clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)),
digestReturns: digestReturns{
err: fmt.Errorf("error"),
},
metadata: policy.Metadata{
Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`,
DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
},
want: false,
wantErr: true,
},
{
name: "sad: non-existent metadata",
clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
want: true,
},
{
name: "sad: broken metadata",
clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
metadata: `"foo"`,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up a temporary directory
tmpDir := t.TempDir()
// Mock image
img := new(fakei.FakeImage)
img.LayersReturns([]v1.Layer{newFakeLayer(t)}, nil)
img.DigestReturns(tt.digestReturns.h, tt.digestReturns.err)
img.ManifestReturns(&v1.Manifest{
Layers: []v1.Descriptor{
{
MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
Size: 100,
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8"},
Annotations: map[string]string{
"org.opencontainers.image.title": "bundle.tar.gz",
},
},
},
}, nil)
// Create a policy directory
err := os.MkdirAll(filepath.Join(tmpDir, "policy"), os.ModePerm)
require.NoError(t, err)
if tt.metadata != nil {
b, err := json.Marshal(tt.metadata)
require.NoError(t, err)
// Write a metadata file
metadataPath := filepath.Join(tmpDir, "policy", "metadata.json")
err = os.WriteFile(metadataPath, b, os.ModePerm)
require.NoError(t, err)
}
mediaType := "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip"
art, err := oci.NewArtifact("repo", mediaType, true, true, oci.WithImage(img))
require.NoError(t, err)
c, err := policy.NewClient(tmpDir, true, policy.WithOCIArtifact(art), policy.WithClock(tt.clock))
require.NoError(t, err)
// Assert results
got, err := c.NeedsUpdate()
assert.Equal(t, tt.wantErr, err != nil)
assert.Equal(t, tt.want, got)
})
}
}
func TestClient_DownloadBuiltinPolicies(t *testing.T) {
type digestReturns struct {
h v1.Hash
err error
}
type layersReturns struct {
layers []v1.Layer
err error
}
tests := []struct {
name string
clock clock.Clock
layersReturns layersReturns
digestReturns digestReturns
want *policy.Metadata
wantErr string
}{
{
name: "happy path",
clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
layersReturns: layersReturns{
layers: []v1.Layer{newFakeLayer(t)},
},
digestReturns: digestReturns{
h: v1.Hash{Algorithm: "sha256", Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d"},
},
want: &policy.Metadata{
Digest: "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC),
},
},
{
name: "sad: broken layer",
clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
layersReturns: layersReturns{
layers: []v1.Layer{newBrokenLayer(t)},
},
digestReturns: digestReturns{
h: v1.Hash{Algorithm: "sha256", Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d"},
},
wantErr: "compressed error",
},
{
name: "sad: Digest returns an error",
clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
layersReturns: layersReturns{
layers: []v1.Layer{newFakeLayer(t)},
},
digestReturns: digestReturns{
err: fmt.Errorf("error"),
},
want: &policy.Metadata{
Digest: "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC),
},
wantErr: "digest error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
// Mock image
img := new(fakei.FakeImage)
img.DigestReturns(tt.digestReturns.h, tt.digestReturns.err)
img.LayersReturns(tt.layersReturns.layers, tt.layersReturns.err)
img.ManifestReturns(&v1.Manifest{
Layers: []v1.Descriptor{
{
MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
Size: 100,
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8"},
Annotations: map[string]string{
"org.opencontainers.image.title": "bundle.tar.gz",
},
},
},
}, nil)
// Mock OCI artifact
mediaType := "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip"
art, err := oci.NewArtifact("repo", mediaType, true, true, oci.WithImage(img))
require.NoError(t, err)
c, err := policy.NewClient(tempDir, true, policy.WithClock(tt.clock), policy.WithOCIArtifact(art))
require.NoError(t, err)
err = c.DownloadBuiltinPolicies(context.Background())
if tt.wantErr != "" {
require.NotNil(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
assert.NoError(t, err)
// Assert metadata.json
metadata := filepath.Join(tempDir, "policy", "metadata.json")
b, err := os.ReadFile(metadata)
require.NoError(t, err)
got := new(policy.Metadata)
err = json.Unmarshal(b, got)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,3 @@
{
"revision": "1",
}

BIN
pkg/policy/testdata/bundle.tar.gz vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
{
"revision": "1"
}

View File

@@ -0,0 +1,4 @@
{
"revision": "1",
"roots": ["kubernetes", "docker"]
}