github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/cache/redis_test.go (about)

     1  package cache_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/alicebob/miniredis/v2"
    10  	"github.com/go-redis/redis/v8"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/devseccon/trivy/pkg/fanal/cache"
    15  	"github.com/devseccon/trivy/pkg/fanal/types"
    16  )
    17  
    18  const correctHash = "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7"
    19  
    20  func TestRedisCache_PutArtifact(t *testing.T) {
    21  	type args struct {
    22  		artifactID     string
    23  		artifactConfig types.ArtifactInfo
    24  	}
    25  	tests := []struct {
    26  		name       string
    27  		setupRedis bool
    28  		args       args
    29  		wantKey    string
    30  		wantErr    string
    31  	}{
    32  		{
    33  			name:       "happy path",
    34  			setupRedis: true,
    35  			args: args{
    36  				artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
    37  				artifactConfig: types.ArtifactInfo{
    38  					SchemaVersion: 2,
    39  					Architecture:  "amd64",
    40  					Created:       time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC),
    41  					DockerVersion: "19.03.12",
    42  					OS:            "linux",
    43  				},
    44  			},
    45  			wantKey: "fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
    46  		},
    47  		{
    48  			name:       "no such host",
    49  			setupRedis: false,
    50  			args: args{
    51  				artifactID:     "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
    52  				artifactConfig: types.ArtifactInfo{},
    53  			},
    54  			wantErr: "unable to store artifact information in Redis cache",
    55  		},
    56  	}
    57  
    58  	// Set up Redis test server
    59  	s, err := miniredis.Run()
    60  	require.NoError(t, err)
    61  	defer s.Close()
    62  
    63  	for _, tt := range tests {
    64  		t.Run(tt.name, func(t *testing.T) {
    65  			addr := s.Addr()
    66  			if !tt.setupRedis {
    67  				addr = "dummy:16379"
    68  			}
    69  
    70  			c := cache.NewRedisCache(&redis.Options{
    71  				Addr: addr,
    72  			}, 0)
    73  
    74  			err = c.PutArtifact(tt.args.artifactID, tt.args.artifactConfig)
    75  			if tt.wantErr != "" {
    76  				require.NotNil(t, err)
    77  				assert.Contains(t, err.Error(), tt.wantErr)
    78  				return
    79  			} else {
    80  				assert.NoError(t, err)
    81  			}
    82  
    83  			got, err := s.Get(tt.wantKey)
    84  			require.NoError(t, err)
    85  
    86  			want, err := json.Marshal(tt.args.artifactConfig)
    87  			require.NoError(t, err)
    88  
    89  			assert.JSONEq(t, string(want), got)
    90  		})
    91  	}
    92  }
    93  
    94  func TestRedisCache_PutBlob(t *testing.T) {
    95  	type args struct {
    96  		blobID     string
    97  		blobConfig types.BlobInfo
    98  	}
    99  	tests := []struct {
   100  		name       string
   101  		setupRedis bool
   102  		args       args
   103  		wantKey    string
   104  		wantErr    string
   105  	}{
   106  		{
   107  			name:       "happy path",
   108  			setupRedis: true,
   109  			args: args{
   110  				blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   111  				blobConfig: types.BlobInfo{
   112  					SchemaVersion: 2,
   113  					Digest:        "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609",
   114  					DiffID:        "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   115  					OS: types.OS{
   116  						Family: "alpine",
   117  						Name:   "3.10.2",
   118  					},
   119  					PackageInfos: []types.PackageInfo{
   120  						{
   121  							FilePath: "lib/apk/db/installed",
   122  							Packages: []types.Package{
   123  								{
   124  									Name:       "musl",
   125  									Version:    "1.1.22-r3",
   126  									SrcName:    "musl",
   127  									SrcVersion: "1.1.22-r3",
   128  								},
   129  							},
   130  						},
   131  					},
   132  				},
   133  			},
   134  			wantKey: "fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   135  		},
   136  		{
   137  			name:       "no such host",
   138  			setupRedis: false,
   139  			args: args{
   140  				blobID:     "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   141  				blobConfig: types.BlobInfo{},
   142  			},
   143  			wantErr: "unable to store blob information in Redis cache",
   144  		},
   145  	}
   146  
   147  	// Set up Redis test server
   148  	s, err := miniredis.Run()
   149  	require.NoError(t, err)
   150  	defer s.Close()
   151  
   152  	for _, tt := range tests {
   153  		t.Run(tt.name, func(t *testing.T) {
   154  			addr := s.Addr()
   155  			if !tt.setupRedis {
   156  				addr = "dummy:16379"
   157  			}
   158  
   159  			c := cache.NewRedisCache(&redis.Options{
   160  				Addr: addr,
   161  			}, 0)
   162  
   163  			err = c.PutBlob(tt.args.blobID, tt.args.blobConfig)
   164  			if tt.wantErr != "" {
   165  				require.NotNil(t, err)
   166  				assert.Contains(t, err.Error(), tt.wantErr)
   167  				return
   168  			} else {
   169  				assert.NoError(t, err)
   170  			}
   171  
   172  			got, err := s.Get(tt.wantKey)
   173  			require.NoError(t, err)
   174  
   175  			want, err := json.Marshal(tt.args.blobConfig)
   176  			require.NoError(t, err)
   177  
   178  			assert.JSONEq(t, string(want), got)
   179  		})
   180  	}
   181  }
   182  
   183  func TestRedisCache_GetArtifact(t *testing.T) {
   184  	info := types.ArtifactInfo{
   185  		SchemaVersion: 2,
   186  		Architecture:  "amd64",
   187  		Created:       time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC),
   188  		DockerVersion: "19.03.12",
   189  		OS:            "linux",
   190  	}
   191  
   192  	tests := []struct {
   193  		name       string
   194  		setupRedis bool
   195  		artifactID string
   196  		want       types.ArtifactInfo
   197  		wantErr    string
   198  	}{
   199  		{
   200  			name:       "happy path",
   201  			setupRedis: true,
   202  			artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
   203  			want:       info,
   204  		},
   205  		{
   206  			name:       "malformed JSON",
   207  			setupRedis: true,
   208  			artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
   209  			wantErr:    "failed to unmarshal artifact",
   210  		},
   211  		{
   212  			name:       "no such host",
   213  			setupRedis: false,
   214  			artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
   215  			wantErr:    "failed to get artifact from the Redis cache",
   216  		},
   217  		{
   218  			name:       "nonexistent key",
   219  			setupRedis: true,
   220  			artifactID: "sha256:foo",
   221  			wantErr:    "artifact (sha256:foo) is missing in Redis cache",
   222  		},
   223  	}
   224  
   225  	// Set up Redis test server
   226  	s, err := miniredis.Run()
   227  	require.NoError(t, err)
   228  	defer s.Close()
   229  
   230  	// Set key/value pairs
   231  	b, err := json.Marshal(info)
   232  	require.NoError(t, err)
   233  
   234  	s.Set("fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", string(b))
   235  	s.Set("fanal::artifact::sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", "foobar")
   236  
   237  	for _, tt := range tests {
   238  		t.Run(tt.name, func(t *testing.T) {
   239  			addr := s.Addr()
   240  			if !tt.setupRedis {
   241  				addr = "dummy:16379"
   242  			}
   243  
   244  			c := cache.NewRedisCache(&redis.Options{
   245  				Addr: addr,
   246  			}, 0)
   247  
   248  			got, err := c.GetArtifact(tt.artifactID)
   249  			if tt.wantErr != "" {
   250  				require.NotNil(t, err)
   251  				assert.Contains(t, err.Error(), tt.wantErr)
   252  				return
   253  			} else {
   254  				assert.NoError(t, err)
   255  			}
   256  
   257  			assert.Equal(t, tt.want, got)
   258  		})
   259  	}
   260  }
   261  
   262  func TestRedisCache_GetBlob(t *testing.T) {
   263  	blobInfo := types.BlobInfo{
   264  		SchemaVersion: 2,
   265  		Digest:        "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609",
   266  		DiffID:        "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   267  		OS: types.OS{
   268  			Family: "alpine",
   269  			Name:   "3.10.2",
   270  		},
   271  		PackageInfos: []types.PackageInfo{
   272  			{
   273  				FilePath: "lib/apk/db/installed",
   274  				Packages: []types.Package{
   275  					{
   276  						Name:       "musl",
   277  						Version:    "1.1.22-r3",
   278  						SrcName:    "musl",
   279  						SrcVersion: "1.1.22-r3",
   280  					},
   281  				},
   282  			},
   283  		},
   284  	}
   285  
   286  	tests := []struct {
   287  		name       string
   288  		setupRedis bool
   289  		blobID     string
   290  		want       types.BlobInfo
   291  		wantErr    string
   292  	}{
   293  		{
   294  			name:       "happy path",
   295  			setupRedis: true,
   296  			blobID:     "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   297  			want:       blobInfo,
   298  		},
   299  		{
   300  			name:       "malformed JSON",
   301  			setupRedis: true,
   302  			blobID:     "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
   303  			wantErr:    "failed to unmarshal blob",
   304  		},
   305  		{
   306  			name:       "no such host",
   307  			setupRedis: false,
   308  			blobID:     "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
   309  			wantErr:    "failed to get blob from the Redis cache",
   310  		},
   311  		{
   312  			name:       "nonexistent key",
   313  			setupRedis: true,
   314  			blobID:     "sha256:foo",
   315  			wantErr:    "blob (sha256:foo) is missing in Redis cache",
   316  		},
   317  	}
   318  
   319  	// Set up Redis test server
   320  	s, err := miniredis.Run()
   321  	require.NoError(t, err)
   322  	defer s.Close()
   323  
   324  	// Set key/value pairs
   325  	b, err := json.Marshal(blobInfo)
   326  	require.NoError(t, err)
   327  	s.Set("fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", string(b))
   328  	s.Set("fanal::blob::sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", "foobar")
   329  
   330  	for _, tt := range tests {
   331  		t.Run(tt.name, func(t *testing.T) {
   332  			addr := s.Addr()
   333  			if !tt.setupRedis {
   334  				addr = "dummy:16379"
   335  			}
   336  
   337  			c := cache.NewRedisCache(&redis.Options{
   338  				Addr: addr,
   339  			}, 0)
   340  
   341  			got, err := c.GetBlob(tt.blobID)
   342  			if tt.wantErr != "" {
   343  				require.NotNil(t, err)
   344  				assert.Contains(t, err.Error(), tt.wantErr)
   345  				return
   346  			}
   347  
   348  			assert.NoError(t, err)
   349  			assert.Equal(t, tt.want, got)
   350  		})
   351  	}
   352  }
   353  
   354  func TestRedisCache_MissingBlobs(t *testing.T) {
   355  	type args struct {
   356  		artifactID string
   357  		blobIDs    []string
   358  	}
   359  	tests := []struct {
   360  		name                string
   361  		setupRedis          bool
   362  		args                args
   363  		wantMissingArtifact bool
   364  		wantMissingBlobIDs  []string
   365  		wantErr             string
   366  	}{
   367  		{
   368  			name:       "missing both",
   369  			setupRedis: true,
   370  			args: args{
   371  				artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4/1",
   372  				blobIDs:    []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
   373  			},
   374  			wantMissingArtifact: true,
   375  			wantMissingBlobIDs:  []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
   376  		},
   377  		{
   378  			name:       "missing artifact",
   379  			setupRedis: true,
   380  			args: args{
   381  				artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4/1",
   382  				blobIDs:    []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111"},
   383  			},
   384  			wantMissingArtifact: true,
   385  		},
   386  		{
   387  			name:       "missing blobs",
   388  			setupRedis: true,
   389  			args: args{
   390  				artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1",
   391  				blobIDs:    []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
   392  			},
   393  			wantMissingArtifact: false,
   394  			wantMissingBlobIDs:  []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
   395  		},
   396  		{
   397  			name:       "missing artifact with different schema version",
   398  			setupRedis: true,
   399  			args: args{
   400  				artifactID: "sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/1",
   401  				blobIDs:    []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111"},
   402  			},
   403  			wantMissingArtifact: true,
   404  		},
   405  		{
   406  			name:       "missing blobs with different schema version",
   407  			setupRedis: true,
   408  			args: args{
   409  				artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1",
   410  				blobIDs:    []string{"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111"},
   411  			},
   412  			wantMissingArtifact: false,
   413  			wantMissingBlobIDs:  []string{"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111"},
   414  		},
   415  		{
   416  			name:       "different analyzer versions",
   417  			setupRedis: true,
   418  			args: args{
   419  				artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/0",
   420  				blobIDs:    []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11012"},
   421  			},
   422  			wantMissingArtifact: true,
   423  			wantMissingBlobIDs:  []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11012"},
   424  		},
   425  	}
   426  
   427  	// Set up Redis test server
   428  	s, err := miniredis.Run()
   429  	require.NoError(t, err)
   430  	defer s.Close()
   431  
   432  	s.Set("fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1",
   433  		fmt.Sprintf("{\"SchemaVersion\": %d}", types.ArtifactJSONSchemaVersion))
   434  	s.Set("fanal::artifact::sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/1",
   435  		`{"SchemaVersion": 999999}`) // This version should not match the current version
   436  	s.Set("fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111",
   437  		fmt.Sprintf("{\"SchemaVersion\": %d}", types.BlobJSONSchemaVersion))
   438  	s.Set("fanal::blob::sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111",
   439  		`{"SchemaVersion": 999999}`) // This version should not match the current version
   440  
   441  	for _, tt := range tests {
   442  		t.Run(tt.name, func(t *testing.T) {
   443  			addr := s.Addr()
   444  			if !tt.setupRedis {
   445  				addr = "dummy:6379"
   446  			}
   447  
   448  			c := cache.NewRedisCache(&redis.Options{
   449  				Addr: addr,
   450  			}, 0)
   451  
   452  			missingArtifact, missingBlobIDs, err := c.MissingBlobs(tt.args.artifactID, tt.args.blobIDs)
   453  			if tt.wantErr != "" {
   454  				require.NotNil(t, err)
   455  				assert.Contains(t, err.Error(), tt.wantErr)
   456  				return
   457  			}
   458  
   459  			assert.NoError(t, err)
   460  			assert.Equal(t, tt.wantMissingArtifact, missingArtifact)
   461  			assert.Equal(t, tt.wantMissingBlobIDs, missingBlobIDs)
   462  		})
   463  	}
   464  }
   465  
   466  func TestRedisCache_Close(t *testing.T) {
   467  	// Set up Redis test server
   468  	s, err := miniredis.Run()
   469  	require.NoError(t, err)
   470  	defer s.Close()
   471  
   472  	t.Run("close", func(t *testing.T) {
   473  		c := cache.NewRedisCache(&redis.Options{
   474  			Addr: s.Addr(),
   475  		}, 0)
   476  		closeErr := c.Close()
   477  		require.NoError(t, closeErr)
   478  		time.Sleep(3 * time.Second) // give it some time
   479  		assert.Equal(t, 0, s.CurrentConnectionCount(), "The client is disconnected")
   480  	})
   481  }
   482  
   483  func TestRedisCache_Clear(t *testing.T) {
   484  	// Set up Redis test server
   485  	s, err := miniredis.Run()
   486  	require.NoError(t, err)
   487  	defer s.Close()
   488  
   489  	for i := 0; i < 200; i++ {
   490  		s.Set(fmt.Sprintf("fanal::key%d", i), "value")
   491  	}
   492  	s.Set("foo", "bar")
   493  
   494  	t.Run("clear", func(t *testing.T) {
   495  		c := cache.NewRedisCache(&redis.Options{
   496  			Addr: s.Addr(),
   497  		}, 0)
   498  		require.NoError(t, c.Clear())
   499  		for i := 0; i < 200; i++ {
   500  			assert.False(t, s.Exists(fmt.Sprintf("fanal::key%d", i)))
   501  		}
   502  		assert.True(t, s.Exists("foo"))
   503  	})
   504  }
   505  
   506  func TestRedisCache_DeleteBlobs(t *testing.T) {
   507  	type args struct {
   508  		blobIDs []string
   509  	}
   510  	tests := []struct {
   511  		name       string
   512  		setupRedis bool
   513  		args       args
   514  		wantKey    string
   515  		wantErr    string
   516  	}{
   517  		{
   518  			name:       "happy path",
   519  			setupRedis: true,
   520  			args: args{
   521  				blobIDs: []string{correctHash},
   522  			},
   523  			wantKey: "fanal::blob::" + correctHash,
   524  		},
   525  		{
   526  			name:       "no such host",
   527  			setupRedis: false,
   528  			args: args{
   529  				blobIDs: []string{"sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae800"},
   530  			},
   531  			wantErr: "unable to delete blob",
   532  		},
   533  	}
   534  
   535  	// Set up Redis test server
   536  	s, err := miniredis.Run()
   537  	require.NoError(t, err)
   538  	defer s.Close()
   539  
   540  	s.Set(correctHash, "any string")
   541  
   542  	for _, tt := range tests {
   543  		t.Run(tt.name, func(t *testing.T) {
   544  			addr := s.Addr()
   545  			if !tt.setupRedis {
   546  				addr = "dummy:16379"
   547  			}
   548  
   549  			c := cache.NewRedisCache(&redis.Options{
   550  				Addr: addr,
   551  			}, 0)
   552  
   553  			err = c.DeleteBlobs(tt.args.blobIDs)
   554  			if tt.wantErr != "" {
   555  				require.Error(t, err)
   556  				assert.Contains(t, err.Error(), tt.wantErr)
   557  				return
   558  			}
   559  			assert.NoError(t, err)
   560  		})
   561  	}
   562  }