github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/cache/fs_test.go (about) 1 package cache 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 bolt "go.etcd.io/bbolt" 15 16 "github.com/devseccon/trivy/pkg/fanal/types" 17 ) 18 19 func newTempDB(t *testing.T, dbPath string) (string, error) { 20 dir := t.TempDir() 21 if dbPath != "" { 22 d := filepath.Join(dir, "fanal") 23 if err := os.MkdirAll(d, 0700); err != nil { 24 return "", err 25 } 26 27 dst := filepath.Join(d, "fanal.db") 28 if _, err := copyFile(dbPath, dst); err != nil { 29 return "", err 30 } 31 } 32 33 return dir, nil 34 } 35 36 func copyFile(src, dst string) (int64, error) { 37 sourceFileStat, err := os.Stat(src) 38 if err != nil { 39 return 0, err 40 } 41 42 if !sourceFileStat.Mode().IsRegular() { 43 return 0, fmt.Errorf("%s is not a regular file", src) 44 } 45 46 source, err := os.Open(src) 47 if err != nil { 48 return 0, err 49 } 50 defer source.Close() 51 52 destination, err := os.Create(dst) 53 if err != nil { 54 return 0, err 55 } 56 defer destination.Close() 57 n, err := io.Copy(destination, source) 58 return n, err 59 } 60 61 func TestFSCache_GetBlob(t *testing.T) { 62 type args struct { 63 layerID string 64 } 65 tests := []struct { 66 name string 67 dbPath string 68 args args 69 want types.BlobInfo 70 wantErr bool 71 }{ 72 { 73 name: "happy path", 74 dbPath: "testdata/fanal.db", 75 args: args{ 76 layerID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11101", 77 }, 78 want: types.BlobInfo{ 79 SchemaVersion: 2, 80 OS: types.OS{ 81 Family: "alpine", 82 Name: "3.10", 83 }, 84 }, 85 }, 86 { 87 name: "sad path", 88 dbPath: "testdata/fanal.db", 89 args: args{ 90 layerID: "sha256:unknown", 91 }, 92 wantErr: true, 93 }, 94 } 95 for _, tt := range tests { 96 t.Run(tt.name, func(t *testing.T) { 97 tmpDir, err := newTempDB(t, tt.dbPath) 98 require.NoError(t, err) 99 100 fs, err := NewFSCache(tmpDir) 101 require.NoError(t, err) 102 defer func() { 103 _ = fs.Clear() 104 _ = fs.Close() 105 }() 106 107 got, err := fs.GetBlob(tt.args.layerID) 108 assert.Equal(t, tt.wantErr, err != nil, err) 109 assert.Equal(t, tt.want, got) 110 }) 111 } 112 } 113 114 func TestFSCache_PutBlob(t *testing.T) { 115 type fields struct { 116 db *bolt.DB 117 directory string 118 } 119 type args struct { 120 diffID string 121 layerInfo types.BlobInfo 122 } 123 tests := []struct { 124 name string 125 fields fields 126 args args 127 want string 128 wantLayerID string 129 wantErr string 130 }{ 131 { 132 name: "happy path", 133 args: args{ 134 diffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", 135 layerInfo: types.BlobInfo{ 136 SchemaVersion: 1, 137 OS: types.OS{ 138 Family: "alpine", 139 Name: "3.10", 140 }, 141 Repository: &types.Repository{ 142 Family: "alpine", 143 Release: "3.10", 144 }, 145 }, 146 }, 147 want: ` 148 { 149 "SchemaVersion": 1, 150 "OS": { 151 "Family": "alpine", 152 "Name": "3.10" 153 }, 154 "Repository": { 155 "Family": "alpine", 156 "Release": "3.10" 157 } 158 }`, 159 wantLayerID: "", 160 }, 161 { 162 name: "happy path: different decompressed layer ID", 163 args: args{ 164 diffID: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", 165 layerInfo: types.BlobInfo{ 166 SchemaVersion: 1, 167 Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", 168 DiffID: "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec", 169 OS: types.OS{ 170 Family: "alpine", 171 Name: "3.10", 172 }, 173 Repository: &types.Repository{ 174 Family: "alpine", 175 Release: "3.10", 176 }, 177 PackageInfos: []types.PackageInfo{ 178 { 179 FilePath: "lib/apk/db/installed", 180 Packages: types.Packages{ 181 { 182 Name: "musl", 183 Version: "1.1.22-r3", 184 }, 185 }, 186 }, 187 }, 188 Applications: []types.Application{ 189 { 190 Type: "composer", 191 FilePath: "php-app/composer.lock", 192 Libraries: types.Packages{ 193 { 194 Name: "guzzlehttp/guzzle", 195 Version: "6.2.0", 196 }, 197 { 198 Name: "guzzlehttp/promises", 199 Version: "v1.3.1", 200 }, 201 }, 202 }, 203 }, 204 OpaqueDirs: []string{"php-app/"}, 205 WhiteoutFiles: []string{"etc/foobar"}, 206 }, 207 }, 208 want: ` 209 { 210 "SchemaVersion": 1, 211 "Digest": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", 212 "DiffID": "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec", 213 "OS": { 214 "Family": "alpine", 215 "Name": "3.10" 216 }, 217 "Repository": { 218 "Family": "alpine", 219 "Release": "3.10" 220 }, 221 "PackageInfos": [ 222 { 223 "FilePath": "lib/apk/db/installed", 224 "Packages": [ 225 { 226 "Name": "musl", 227 "Version": "1.1.22-r3", 228 "Layer": {} 229 } 230 ] 231 } 232 ], 233 "Applications": [ 234 { 235 "Type": "composer", 236 "FilePath": "php-app/composer.lock", 237 "Libraries": [ 238 { 239 "Name":"guzzlehttp/guzzle", 240 "Version":"6.2.0", 241 "Layer": {} 242 }, 243 { 244 "Name":"guzzlehttp/promises", 245 "Version":"v1.3.1", 246 "Layer": {} 247 } 248 ] 249 } 250 ], 251 "OpaqueDirs": [ 252 "php-app/" 253 ], 254 "WhiteoutFiles": [ 255 "etc/foobar" 256 ] 257 }`, 258 wantLayerID: "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec", 259 }, 260 { 261 name: "sad path closed DB", 262 args: args{ 263 diffID: "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec/11111", 264 }, 265 wantErr: "database not open", 266 }, 267 } 268 for _, tt := range tests { 269 t.Run(tt.name, func(t *testing.T) { 270 tmpDir, err := newTempDB(t, "") 271 require.NoError(t, err) 272 273 fs, err := NewFSCache(tmpDir) 274 require.NoError(t, err) 275 defer func() { 276 _ = fs.Clear() 277 _ = fs.Close() 278 }() 279 280 if strings.HasPrefix(tt.name, "sad") { 281 require.NoError(t, fs.Close()) 282 } 283 284 err = fs.PutBlob(tt.args.diffID, tt.args.layerInfo) 285 if tt.wantErr != "" { 286 require.NotNil(t, err) 287 assert.Contains(t, err.Error(), tt.wantErr, tt.name) 288 return 289 } else { 290 require.NoError(t, err, tt.name) 291 } 292 293 fs.db.View(func(tx *bolt.Tx) error { 294 layerBucket := tx.Bucket([]byte(blobBucket)) 295 b := layerBucket.Get([]byte(tt.args.diffID)) 296 assert.JSONEq(t, tt.want, string(b)) 297 298 return nil 299 }) 300 }) 301 } 302 } 303 304 func TestFSCache_PutArtifact(t *testing.T) { 305 type args struct { 306 imageID string 307 imageConfig types.ArtifactInfo 308 } 309 tests := []struct { 310 name string 311 args args 312 want string 313 wantErr string 314 }{ 315 { 316 name: "happy path", 317 args: args{ 318 imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", 319 imageConfig: types.ArtifactInfo{ 320 SchemaVersion: 1, 321 Architecture: "amd64", 322 Created: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), 323 DockerVersion: "18.06.1-ce", 324 OS: "linux", 325 HistoryPackages: types.Packages{ 326 { 327 Name: "musl", 328 Version: "1.2.3", 329 }, 330 }, 331 }, 332 }, 333 want: ` 334 { 335 "SchemaVersion": 1, 336 "Architecture": "amd64", 337 "Created": "2020-01-02T03:04:05Z", 338 "DockerVersion": "18.06.1-ce", 339 "OS": "linux", 340 "HistoryPackages": [ 341 { 342 "Name": "musl", 343 "Version": "1.2.3", 344 "Layer": {} 345 } 346 ] 347 } 348 `, 349 }, 350 } 351 for _, tt := range tests { 352 t.Run(tt.name, func(t *testing.T) { 353 tmpDir, err := newTempDB(t, "") 354 require.NoError(t, err) 355 356 fs, err := NewFSCache(tmpDir) 357 require.NoError(t, err) 358 defer func() { 359 _ = fs.Clear() 360 _ = fs.Close() 361 }() 362 363 err = fs.PutArtifact(tt.args.imageID, tt.args.imageConfig) 364 if tt.wantErr != "" { 365 require.NotNil(t, err) 366 assert.Contains(t, err.Error(), tt.wantErr, tt.name) 367 return 368 } else { 369 require.NoError(t, err, tt.name) 370 } 371 372 fs.db.View(func(tx *bolt.Tx) error { 373 // check decompressedDigestBucket 374 imageBucket := tx.Bucket([]byte(artifactBucket)) 375 b := imageBucket.Get([]byte(tt.args.imageID)) 376 assert.JSONEq(t, tt.want, string(b)) 377 378 return nil 379 }) 380 }) 381 } 382 } 383 384 func TestFSCache_MissingBlobs(t *testing.T) { 385 type args struct { 386 imageID string 387 layerIDs []string 388 } 389 tests := []struct { 390 name string 391 dbPath string 392 args args 393 wantMissingImage bool 394 wantMissingLayerIDs []string 395 wantErr string 396 }{ 397 { 398 name: "happy path", 399 dbPath: "testdata/fanal.db", 400 args: args{ 401 imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4/1", 402 layerIDs: []string{ 403 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11101", 404 "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5/11101", 405 "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec/11101", 406 }, 407 }, 408 wantMissingImage: false, 409 wantMissingLayerIDs: []string{ 410 "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5/11101", 411 "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec/11101", 412 }, 413 }, 414 { 415 name: "happy path: broken layer JSON", 416 dbPath: "testdata/broken-layer.db", 417 args: args{ 418 imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", 419 layerIDs: []string{ 420 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", 421 }, 422 }, 423 wantMissingImage: true, 424 wantMissingLayerIDs: []string{ 425 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", 426 }, 427 }, 428 { 429 name: "happy path: broken image JSON", 430 dbPath: "testdata/broken-image.db", 431 args: args{ 432 imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", 433 layerIDs: []string{ 434 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", 435 }, 436 }, 437 wantMissingImage: true, 438 wantMissingLayerIDs: []string{ 439 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", 440 }, 441 }, 442 { 443 name: "happy path: the schema version of image JSON doesn't match", 444 dbPath: "testdata/different-image-schema.db", 445 args: args{ 446 imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", 447 layerIDs: []string{ 448 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", 449 }, 450 }, 451 wantMissingImage: true, 452 wantMissingLayerIDs: []string{ 453 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", 454 }, 455 }, 456 { 457 name: "happy path: new config analyzer", 458 dbPath: "testdata/fanal.db", 459 args: args{ 460 imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4/2", 461 layerIDs: []string{ 462 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11102", 463 }, 464 }, 465 wantMissingImage: true, 466 wantMissingLayerIDs: []string{ 467 "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11102", 468 }, 469 }, 470 } 471 for _, tt := range tests { 472 t.Run(tt.name, func(t *testing.T) { 473 tmpDir, err := newTempDB(t, tt.dbPath) 474 require.NoError(t, err) 475 476 fs, err := NewFSCache(tmpDir) 477 require.NoError(t, err) 478 defer func() { 479 _ = fs.Clear() 480 _ = fs.Close() 481 }() 482 483 gotMissingImage, gotMissingLayerIDs, err := fs.MissingBlobs(tt.args.imageID, tt.args.layerIDs) 484 if tt.wantErr != "" { 485 assert.ErrorContains(t, err, tt.wantErr, tt.name) 486 return 487 } 488 require.NoError(t, err, tt.name) 489 assert.Equal(t, tt.wantMissingImage, gotMissingImage, tt.name) 490 assert.Equal(t, tt.wantMissingLayerIDs, gotMissingLayerIDs, tt.name) 491 }) 492 } 493 }