mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 07:10:41 -08:00
feat(misconf): Fetch policies from OCI registry (#3015)
Signed-off-by: Simar <simar@linux.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
@@ -28,14 +27,17 @@ func Test_Run(t *testing.T) {
|
||||
regoPolicy string
|
||||
}{
|
||||
{
|
||||
name: "fail without region",
|
||||
options: flag.Options{},
|
||||
name: "fail without region",
|
||||
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"},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -492,20 +492,33 @@ 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) {
|
||||
log.Logger.Info("Misconfiguration scanning is enabled")
|
||||
configScannerOptions = config.ScannerOption{
|
||||
Trace: opts.Trace,
|
||||
Namespaces: append(opts.PolicyNamespaces, defaultPolicyNamespaces...),
|
||||
PolicyPaths: opts.PolicyPaths,
|
||||
DataPaths: opts.DataPaths,
|
||||
HelmValues: opts.HelmValues,
|
||||
HelmValueFiles: opts.HelmValueFiles,
|
||||
HelmFileValues: opts.HelmFileValues,
|
||||
HelmStringValues: opts.HelmStringValues,
|
||||
TerraformTFVars: opts.TerraformTFVars,
|
||||
Trace: opts.Trace,
|
||||
Namespaces: append(opts.PolicyNamespaces, defaultPolicyNamespaces...),
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
216
pkg/policy/policy.go
Normal 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
374
pkg/policy/policy_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
3
pkg/policy/testdata/broken/policy/content/.manifest
vendored
Normal file
3
pkg/policy/testdata/broken/policy/content/.manifest
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"revision": "1",
|
||||
}
|
||||
BIN
pkg/policy/testdata/bundle.tar.gz
vendored
Normal file
BIN
pkg/policy/testdata/bundle.tar.gz
vendored
Normal file
Binary file not shown.
3
pkg/policy/testdata/empty/policy/content/.manifest
vendored
Normal file
3
pkg/policy/testdata/empty/policy/content/.manifest
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"revision": "1"
|
||||
}
|
||||
4
pkg/policy/testdata/happy/policy/content/.manifest
vendored
Normal file
4
pkg/policy/testdata/happy/policy/content/.manifest
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"revision": "1",
|
||||
"roots": ["kubernetes", "docker"]
|
||||
}
|
||||
Reference in New Issue
Block a user