github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/policy/policy_test.go (about)

     1  package policy_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"testing"
    11  	"time"
    12  
    13  	v1 "github.com/google/go-containerregistry/pkg/v1"
    14  	fakei "github.com/google/go-containerregistry/pkg/v1/fake"
    15  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    16  	"github.com/google/go-containerregistry/pkg/v1/types"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  	"k8s.io/utils/clock"
    20  	fake "k8s.io/utils/clock/testing"
    21  
    22  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    23  	"github.com/devseccon/trivy/pkg/oci"
    24  	"github.com/devseccon/trivy/pkg/policy"
    25  )
    26  
    27  type fakeLayer struct {
    28  	v1.Layer
    29  }
    30  
    31  func (f fakeLayer) MediaType() (types.MediaType, error) {
    32  	return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil
    33  }
    34  
    35  func newFakeLayer(t *testing.T) v1.Layer {
    36  	layer, err := tarball.LayerFromFile("testdata/bundle.tar.gz")
    37  	require.NoError(t, err)
    38  	require.NotNil(t, layer)
    39  
    40  	return fakeLayer{layer}
    41  }
    42  
    43  type brokenLayer struct {
    44  	v1.Layer
    45  }
    46  
    47  func (b brokenLayer) MediaType() (types.MediaType, error) {
    48  	return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil
    49  }
    50  
    51  func (b brokenLayer) Compressed() (io.ReadCloser, error) {
    52  	return nil, fmt.Errorf("compressed error")
    53  }
    54  
    55  func newBrokenLayer(t *testing.T) v1.Layer {
    56  	layer, err := tarball.LayerFromFile("testdata/bundle.tar.gz")
    57  	require.NoError(t, err)
    58  
    59  	return brokenLayer{layer}
    60  }
    61  
    62  func TestClient_LoadBuiltinPolicies(t *testing.T) {
    63  	tests := []struct {
    64  		name     string
    65  		cacheDir string
    66  		want     []string
    67  		wantErr  string
    68  	}{
    69  		{
    70  			name:     "happy path",
    71  			cacheDir: "testdata/happy",
    72  			want: []string{
    73  				filepath.Join("testdata/happy/policy/content/kubernetes"),
    74  				filepath.Join("testdata/happy/policy/content/docker"),
    75  			},
    76  		},
    77  		{
    78  			name:     "empty roots",
    79  			cacheDir: "testdata/empty",
    80  			want: []string{
    81  				filepath.Join("testdata/empty/policy/content"),
    82  			},
    83  		},
    84  		{
    85  			name:     "broken manifest",
    86  			cacheDir: "testdata/broken",
    87  			want:     []string{},
    88  			wantErr:  "json decode error",
    89  		},
    90  		{
    91  			name:     "no such file",
    92  			cacheDir: "testdata/unknown",
    93  			want:     []string{},
    94  			wantErr:  "manifest file open error",
    95  		},
    96  	}
    97  	for _, tt := range tests {
    98  		t.Run(tt.name, func(t *testing.T) {
    99  			// Mock image
   100  			img := new(fakei.FakeImage)
   101  			img.LayersReturns([]v1.Layer{newFakeLayer(t)}, nil)
   102  			img.ManifestReturns(&v1.Manifest{
   103  				Layers: []v1.Descriptor{
   104  					{
   105  						MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
   106  						Size:      100,
   107  						Digest: v1.Hash{
   108  							Algorithm: "sha256",
   109  							Hex:       "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8",
   110  						},
   111  						Annotations: map[string]string{
   112  							"org.opencontainers.image.title": "bundle.tar.gz",
   113  						},
   114  					},
   115  				},
   116  			}, nil)
   117  
   118  			// Mock OCI artifact
   119  			art, err := oci.NewArtifact("repo", true, ftypes.RegistryOptions{}, oci.WithImage(img))
   120  			require.NoError(t, err)
   121  
   122  			c, err := policy.NewClient(tt.cacheDir, true, "", policy.WithOCIArtifact(art))
   123  			require.NoError(t, err)
   124  
   125  			got, err := c.LoadBuiltinPolicies()
   126  			if tt.wantErr != "" {
   127  				require.NotNil(t, err)
   128  				assert.Contains(t, err.Error(), tt.wantErr)
   129  				return
   130  			}
   131  			assert.NoError(t, err)
   132  			assert.Equal(t, tt.want, got)
   133  		})
   134  	}
   135  }
   136  
   137  func TestClient_NeedsUpdate(t *testing.T) {
   138  	type digestReturns struct {
   139  		h   v1.Hash
   140  		err error
   141  	}
   142  	tests := []struct {
   143  		name          string
   144  		clock         clock.Clock
   145  		digestReturns digestReturns
   146  		metadata      interface{}
   147  		want          bool
   148  		wantErr       bool
   149  	}{
   150  		{
   151  			name:  "recent download",
   152  			clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
   153  			digestReturns: digestReturns{
   154  				h: v1.Hash{
   155  					Algorithm: "sha256",
   156  					Hex:       "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
   157  				},
   158  			},
   159  			metadata: policy.Metadata{
   160  				Digest:       `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`,
   161  				DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
   162  			},
   163  			want: false,
   164  		},
   165  		{
   166  			name:  "same digest",
   167  			clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)),
   168  			digestReturns: digestReturns{
   169  				h: v1.Hash{
   170  					Algorithm: "sha256",
   171  					Hex:       "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
   172  				},
   173  			},
   174  			metadata: policy.Metadata{
   175  				Digest:       `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`,
   176  				DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
   177  			},
   178  			want: false,
   179  		},
   180  		{
   181  			name:  "different digest",
   182  			clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)),
   183  			digestReturns: digestReturns{
   184  				h: v1.Hash{
   185  					Algorithm: "sha256",
   186  					Hex:       "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
   187  				},
   188  			},
   189  			metadata: policy.Metadata{
   190  				Digest:       `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`,
   191  				DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
   192  			},
   193  			want: true,
   194  		},
   195  		{
   196  			name:  "sad: Digest returns  an error",
   197  			clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)),
   198  			digestReturns: digestReturns{
   199  				err: fmt.Errorf("error"),
   200  			},
   201  			metadata: policy.Metadata{
   202  				Digest:       `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`,
   203  				DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
   204  			},
   205  			want:    false,
   206  			wantErr: true,
   207  		},
   208  		{
   209  			name:  "sad: non-existent metadata",
   210  			clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
   211  			want:  true,
   212  		},
   213  		{
   214  			name:     "sad: broken metadata",
   215  			clock:    fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
   216  			metadata: `"foo"`,
   217  			want:     true,
   218  		},
   219  	}
   220  
   221  	for _, tt := range tests {
   222  		t.Run(tt.name, func(t *testing.T) {
   223  			// Set up a temporary directory
   224  			tmpDir := t.TempDir()
   225  
   226  			// Mock image
   227  			img := new(fakei.FakeImage)
   228  			img.LayersReturns([]v1.Layer{newFakeLayer(t)}, nil)
   229  			img.DigestReturns(tt.digestReturns.h, tt.digestReturns.err)
   230  			img.ManifestReturns(&v1.Manifest{
   231  				Layers: []v1.Descriptor{
   232  					{
   233  						MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
   234  						Size:      100,
   235  						Digest: v1.Hash{
   236  							Algorithm: "sha256",
   237  							Hex:       "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8",
   238  						},
   239  						Annotations: map[string]string{
   240  							"org.opencontainers.image.title": "bundle.tar.gz",
   241  						},
   242  					},
   243  				},
   244  			}, nil)
   245  
   246  			// Create a policy directory
   247  			err := os.MkdirAll(filepath.Join(tmpDir, "policy"), os.ModePerm)
   248  			require.NoError(t, err)
   249  
   250  			if tt.metadata != nil {
   251  				b, err := json.Marshal(tt.metadata)
   252  				require.NoError(t, err)
   253  
   254  				// Write a metadata file
   255  				metadataPath := filepath.Join(tmpDir, "policy", "metadata.json")
   256  				err = os.WriteFile(metadataPath, b, os.ModePerm)
   257  				require.NoError(t, err)
   258  			}
   259  
   260  			art, err := oci.NewArtifact("repo", true, ftypes.RegistryOptions{}, oci.WithImage(img))
   261  			require.NoError(t, err)
   262  
   263  			c, err := policy.NewClient(tmpDir, true, "", policy.WithOCIArtifact(art), policy.WithClock(tt.clock))
   264  			require.NoError(t, err)
   265  
   266  			// Assert results
   267  			got, err := c.NeedsUpdate(context.Background())
   268  			assert.Equal(t, tt.wantErr, err != nil)
   269  			assert.Equal(t, tt.want, got)
   270  		})
   271  	}
   272  }
   273  
   274  func TestClient_DownloadBuiltinPolicies(t *testing.T) {
   275  	type digestReturns struct {
   276  		h   v1.Hash
   277  		err error
   278  	}
   279  	type layersReturns struct {
   280  		layers []v1.Layer
   281  		err    error
   282  	}
   283  	tests := []struct {
   284  		name          string
   285  		clock         clock.Clock
   286  		layersReturns layersReturns
   287  		digestReturns digestReturns
   288  		want          *policy.Metadata
   289  		wantErr       string
   290  	}{
   291  		{
   292  			name:  "happy path",
   293  			clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
   294  			layersReturns: layersReturns{
   295  				layers: []v1.Layer{newFakeLayer(t)},
   296  			},
   297  			digestReturns: digestReturns{
   298  				h: v1.Hash{
   299  					Algorithm: "sha256",
   300  					Hex:       "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
   301  				},
   302  			},
   303  			want: &policy.Metadata{
   304  				Digest:       "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
   305  				DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC),
   306  			},
   307  		},
   308  		{
   309  			name:  "sad: broken layer",
   310  			clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
   311  			layersReturns: layersReturns{
   312  				layers: []v1.Layer{newBrokenLayer(t)},
   313  			},
   314  			digestReturns: digestReturns{
   315  				h: v1.Hash{
   316  					Algorithm: "sha256",
   317  					Hex:       "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
   318  				},
   319  			},
   320  			wantErr: "compressed error",
   321  		},
   322  		{
   323  			name:  "sad: Digest returns an error",
   324  			clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)),
   325  			layersReturns: layersReturns{
   326  				layers: []v1.Layer{newFakeLayer(t)},
   327  			},
   328  			digestReturns: digestReturns{
   329  				err: fmt.Errorf("error"),
   330  			},
   331  			want: &policy.Metadata{
   332  				Digest:       "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d",
   333  				DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC),
   334  			},
   335  			wantErr: "digest error",
   336  		},
   337  	}
   338  
   339  	for _, tt := range tests {
   340  		t.Run(tt.name, func(t *testing.T) {
   341  			tempDir := t.TempDir()
   342  
   343  			// Mock image
   344  			img := new(fakei.FakeImage)
   345  			img.DigestReturns(tt.digestReturns.h, tt.digestReturns.err)
   346  			img.LayersReturns(tt.layersReturns.layers, tt.layersReturns.err)
   347  			img.ManifestReturns(&v1.Manifest{
   348  				Layers: []v1.Descriptor{
   349  					{
   350  						MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
   351  						Size:      100,
   352  						Digest: v1.Hash{
   353  							Algorithm: "sha256",
   354  							Hex:       "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8",
   355  						},
   356  						Annotations: map[string]string{
   357  							"org.opencontainers.image.title": "bundle.tar.gz",
   358  						},
   359  					},
   360  				},
   361  			}, nil)
   362  
   363  			// Mock OCI artifact
   364  			art, err := oci.NewArtifact("repo", true, ftypes.RegistryOptions{}, oci.WithImage(img))
   365  			require.NoError(t, err)
   366  
   367  			c, err := policy.NewClient(tempDir, true, "", policy.WithClock(tt.clock), policy.WithOCIArtifact(art))
   368  			require.NoError(t, err)
   369  
   370  			err = c.DownloadBuiltinPolicies(context.Background())
   371  			if tt.wantErr != "" {
   372  				require.NotNil(t, err)
   373  				assert.Contains(t, err.Error(), tt.wantErr)
   374  				return
   375  			}
   376  			assert.NoError(t, err)
   377  
   378  			// Assert metadata.json
   379  			metadata := filepath.Join(tempDir, "policy", "metadata.json")
   380  			b, err := os.ReadFile(metadata)
   381  			require.NoError(t, err)
   382  
   383  			got := new(policy.Metadata)
   384  			err = json.Unmarshal(b, got)
   385  			require.NoError(t, err)
   386  
   387  			assert.Equal(t, tt.want, got)
   388  		})
   389  	}
   390  }
   391  
   392  func TestClient_Clear(t *testing.T) {
   393  	cacheDir := t.TempDir()
   394  	err := os.MkdirAll(filepath.Join(cacheDir, "policy"), 0755)
   395  	require.NoError(t, err)
   396  
   397  	c, err := policy.NewClient(cacheDir, true, "")
   398  	require.NoError(t, err)
   399  	require.NoError(t, c.Clear())
   400  }