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 }