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  }