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

     1  package analyzer_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"sync"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	"golang.org/x/sync/semaphore"
    13  	"golang.org/x/xerrors"
    14  
    15  	dio "github.com/aquasecurity/go-dep-parser/pkg/io"
    16  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    17  	"github.com/devseccon/trivy/pkg/fanal/types"
    18  	"github.com/devseccon/trivy/pkg/javadb"
    19  	"github.com/devseccon/trivy/pkg/mapfs"
    20  
    21  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/imgconf/apk"
    22  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/language/java/jar"
    23  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/language/python/poetry"
    24  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/language/ruby/bundler"
    25  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/os/alpine"
    26  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/os/ubuntu"
    27  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/pkg/apk"
    28  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/repo/apk"
    29  	_ "github.com/devseccon/trivy/pkg/fanal/handler/all"
    30  	_ "modernc.org/sqlite"
    31  )
    32  
    33  func TestAnalysisResult_Merge(t *testing.T) {
    34  	type fields struct {
    35  		m            sync.Mutex
    36  		OS           types.OS
    37  		PackageInfos []types.PackageInfo
    38  		Applications []types.Application
    39  	}
    40  	type args struct {
    41  		new *analyzer.AnalysisResult
    42  	}
    43  	tests := []struct {
    44  		name   string
    45  		fields fields
    46  		args   args
    47  		want   analyzer.AnalysisResult
    48  	}{
    49  		{
    50  			name: "happy path",
    51  			fields: fields{
    52  				OS: types.OS{
    53  					Family: types.Debian,
    54  					Name:   "9.8",
    55  				},
    56  				PackageInfos: []types.PackageInfo{
    57  					{
    58  						FilePath: "var/lib/dpkg/status.d/libc",
    59  						Packages: types.Packages{
    60  							{
    61  								Name:    "libc",
    62  								Version: "1.2.3",
    63  							},
    64  						},
    65  					},
    66  				},
    67  				Applications: []types.Application{
    68  					{
    69  						Type:     "bundler",
    70  						FilePath: "app/Gemfile.lock",
    71  						Libraries: types.Packages{
    72  							{
    73  								Name:    "rails",
    74  								Version: "5.0.0",
    75  							},
    76  						},
    77  					},
    78  				},
    79  			},
    80  			args: args{
    81  				new: &analyzer.AnalysisResult{
    82  					PackageInfos: []types.PackageInfo{
    83  						{
    84  							FilePath: "var/lib/dpkg/status.d/openssl",
    85  							Packages: types.Packages{
    86  								{
    87  									Name:    "openssl",
    88  									Version: "1.1.1",
    89  								},
    90  							},
    91  						},
    92  					},
    93  					Applications: []types.Application{
    94  						{
    95  							Type:     "bundler",
    96  							FilePath: "app2/Gemfile.lock",
    97  							Libraries: types.Packages{
    98  								{
    99  									Name:    "nokogiri",
   100  									Version: "1.0.0",
   101  								},
   102  							},
   103  						},
   104  					},
   105  				},
   106  			},
   107  			want: analyzer.AnalysisResult{
   108  				OS: types.OS{
   109  					Family: types.Debian,
   110  					Name:   "9.8",
   111  				},
   112  				PackageInfos: []types.PackageInfo{
   113  					{
   114  						FilePath: "var/lib/dpkg/status.d/libc",
   115  						Packages: types.Packages{
   116  							{
   117  								Name:    "libc",
   118  								Version: "1.2.3",
   119  							},
   120  						},
   121  					},
   122  					{
   123  						FilePath: "var/lib/dpkg/status.d/openssl",
   124  						Packages: types.Packages{
   125  							{
   126  								Name:    "openssl",
   127  								Version: "1.1.1",
   128  							},
   129  						},
   130  					},
   131  				},
   132  				Applications: []types.Application{
   133  					{
   134  						Type:     "bundler",
   135  						FilePath: "app/Gemfile.lock",
   136  						Libraries: types.Packages{
   137  							{
   138  								Name:    "rails",
   139  								Version: "5.0.0",
   140  							},
   141  						},
   142  					},
   143  					{
   144  						Type:     "bundler",
   145  						FilePath: "app2/Gemfile.lock",
   146  						Libraries: types.Packages{
   147  							{
   148  								Name:    "nokogiri",
   149  								Version: "1.0.0",
   150  							},
   151  						},
   152  					},
   153  				},
   154  			},
   155  		},
   156  		{
   157  			name: "redhat must be replaced with oracle",
   158  			fields: fields{
   159  				OS: types.OS{
   160  					Family: types.RedHat, // this must be overwritten
   161  					Name:   "8.0",
   162  				},
   163  			},
   164  			args: args{
   165  				new: &analyzer.AnalysisResult{
   166  					OS: types.OS{
   167  						Family: types.Oracle,
   168  						Name:   "8.0",
   169  					},
   170  				},
   171  			},
   172  			want: analyzer.AnalysisResult{
   173  				OS: types.OS{
   174  					Family: types.Oracle,
   175  					Name:   "8.0",
   176  				},
   177  			},
   178  		},
   179  		{
   180  			name: "debian must be replaced with ubuntu",
   181  			fields: fields{
   182  				OS: types.OS{
   183  					Family: types.Debian, // this must be overwritten
   184  					Name:   "9.0",
   185  				},
   186  			},
   187  			args: args{
   188  				new: &analyzer.AnalysisResult{
   189  					OS: types.OS{
   190  						Family: types.Ubuntu,
   191  						Name:   "18.04",
   192  					},
   193  				},
   194  			},
   195  			want: analyzer.AnalysisResult{
   196  				OS: types.OS{
   197  					Family: types.Ubuntu,
   198  					Name:   "18.04",
   199  				},
   200  			},
   201  		},
   202  		{
   203  			name: "merge extended flag",
   204  			fields: fields{
   205  				// This must be overwritten
   206  				OS: types.OS{
   207  					Family: types.Ubuntu,
   208  					Name:   "16.04",
   209  				},
   210  			},
   211  			args: args{
   212  				new: &analyzer.AnalysisResult{
   213  					OS: types.OS{
   214  						Family:   types.Ubuntu,
   215  						Extended: true,
   216  					},
   217  				},
   218  			},
   219  			want: analyzer.AnalysisResult{
   220  				OS: types.OS{
   221  					Family:   types.Ubuntu,
   222  					Name:     "16.04",
   223  					Extended: true,
   224  				},
   225  			},
   226  		},
   227  		{
   228  			name: "alpine OS needs to be extended with apk repositories",
   229  			fields: fields{
   230  				OS: types.OS{
   231  					Family: types.Alpine,
   232  					Name:   "3.15.3",
   233  				},
   234  			},
   235  			args: args{
   236  				new: &analyzer.AnalysisResult{
   237  					Repository: &types.Repository{
   238  						Family:  types.Alpine,
   239  						Release: "edge",
   240  					},
   241  				},
   242  			},
   243  			want: analyzer.AnalysisResult{
   244  				OS: types.OS{
   245  					Family: types.Alpine,
   246  					Name:   "3.15.3",
   247  				},
   248  				Repository: &types.Repository{
   249  					Family:  types.Alpine,
   250  					Release: "edge",
   251  				},
   252  			},
   253  		},
   254  		{
   255  			name: "alpine must not be replaced with oracle",
   256  			fields: fields{
   257  				OS: types.OS{
   258  					Family: types.Alpine, // this must not be overwritten
   259  					Name:   "3.11",
   260  				},
   261  			},
   262  			args: args{
   263  				new: &analyzer.AnalysisResult{
   264  					OS: types.OS{
   265  						Family: types.Oracle,
   266  						Name:   "8.0",
   267  					},
   268  				},
   269  			},
   270  			want: analyzer.AnalysisResult{
   271  				OS: types.OS{
   272  					Family: types.Alpine, // this must not be overwritten
   273  					Name:   "3.11",
   274  				},
   275  			},
   276  		},
   277  	}
   278  	for _, tt := range tests {
   279  		t.Run(tt.name, func(t *testing.T) {
   280  			r := analyzer.AnalysisResult{
   281  				OS:           tt.fields.OS,
   282  				PackageInfos: tt.fields.PackageInfos,
   283  				Applications: tt.fields.Applications,
   284  			}
   285  			r.Merge(tt.args.new)
   286  			assert.Equal(t, tt.want, r)
   287  		})
   288  	}
   289  }
   290  
   291  func TestAnalyzerGroup_AnalyzeFile(t *testing.T) {
   292  	type args struct {
   293  		filePath          string
   294  		testFilePath      string
   295  		disabledAnalyzers []analyzer.Type
   296  		filePatterns      []string
   297  	}
   298  	tests := []struct {
   299  		name    string
   300  		args    args
   301  		want    *analyzer.AnalysisResult
   302  		wantErr string
   303  	}{
   304  		{
   305  			name: "happy path with os analyzer",
   306  			args: args{
   307  				filePath:     "/etc/alpine-release",
   308  				testFilePath: "testdata/etc/alpine-release",
   309  			},
   310  			want: &analyzer.AnalysisResult{
   311  				OS: types.OS{
   312  					Family: "alpine",
   313  					Name:   "3.11.6",
   314  				},
   315  			},
   316  		},
   317  		{
   318  			name: "happy path with disabled os analyzer",
   319  			args: args{
   320  				filePath:          "/etc/alpine-release",
   321  				testFilePath:      "testdata/etc/alpine-release",
   322  				disabledAnalyzers: []analyzer.Type{analyzer.TypeAlpine},
   323  			},
   324  			want: &analyzer.AnalysisResult{},
   325  		},
   326  		{
   327  			name: "happy path with package analyzer",
   328  			args: args{
   329  				filePath:     "/lib/apk/db/installed",
   330  				testFilePath: "testdata/lib/apk/db/installed",
   331  			},
   332  			want: &analyzer.AnalysisResult{
   333  				PackageInfos: []types.PackageInfo{
   334  					{
   335  						FilePath: "/lib/apk/db/installed",
   336  						Packages: types.Packages{
   337  							{
   338  								ID:             "musl@1.1.24-r2",
   339  								Name:           "musl",
   340  								Version:        "1.1.24-r2",
   341  								SrcName:        "musl",
   342  								SrcVersion:     "1.1.24-r2",
   343  								Licenses:       []string{"MIT"},
   344  								Arch:           "x86_64",
   345  								Digest:         "sha1:cb2316a189ebee5282c4a9bd98794cc2477a74c6",
   346  								InstalledFiles: []string{"lib/libc.musl-x86_64.so.1", "lib/ld-musl-x86_64.so.1"},
   347  							},
   348  						},
   349  					},
   350  				},
   351  				SystemInstalledFiles: []string{
   352  					"lib/libc.musl-x86_64.so.1",
   353  					"lib/ld-musl-x86_64.so.1",
   354  				},
   355  			},
   356  		},
   357  		{
   358  			name: "happy path with disabled package analyzer",
   359  			args: args{
   360  				filePath:          "/lib/apk/db/installed",
   361  				testFilePath:      "testdata/lib/apk/db/installed",
   362  				disabledAnalyzers: []analyzer.Type{analyzer.TypeApk},
   363  			},
   364  			want: &analyzer.AnalysisResult{},
   365  		},
   366  		{
   367  			name: "happy path with library analyzer",
   368  			args: args{
   369  				filePath:     "/app/Gemfile.lock",
   370  				testFilePath: "testdata/app/Gemfile.lock",
   371  			},
   372  			want: &analyzer.AnalysisResult{
   373  				Applications: []types.Application{
   374  					{
   375  						Type:     "bundler",
   376  						FilePath: "/app/Gemfile.lock",
   377  						Libraries: types.Packages{
   378  							{
   379  								ID:       "actioncable@5.2.3",
   380  								Name:     "actioncable",
   381  								Version:  "5.2.3",
   382  								Indirect: false,
   383  								DependsOn: []string{
   384  									"actionpack@5.2.3",
   385  								},
   386  								Locations: []types.Location{
   387  									{
   388  										StartLine: 4,
   389  										EndLine:   4,
   390  									},
   391  								},
   392  							},
   393  							{
   394  								ID:       "actionpack@5.2.3",
   395  								Name:     "actionpack",
   396  								Version:  "5.2.3",
   397  								Indirect: true,
   398  								Locations: []types.Location{
   399  									{
   400  										StartLine: 6,
   401  										EndLine:   6,
   402  									},
   403  								},
   404  							},
   405  						},
   406  					},
   407  				},
   408  			},
   409  		},
   410  		{
   411  			name: "happy path with invalid os information",
   412  			args: args{
   413  				filePath:     "/etc/lsb-release",
   414  				testFilePath: "testdata/etc/hostname",
   415  			},
   416  			want: &analyzer.AnalysisResult{},
   417  		},
   418  		{
   419  			name: "happy path with a directory",
   420  			args: args{
   421  				filePath:     "/etc/lsb-release",
   422  				testFilePath: "testdata/etc",
   423  			},
   424  			want: &analyzer.AnalysisResult{},
   425  		},
   426  		{
   427  			name: "happy path with library analyzer file pattern regex",
   428  			args: args{
   429  				filePath:     "/app/Gemfile-dev.lock",
   430  				testFilePath: "testdata/app/Gemfile.lock",
   431  				filePatterns: []string{"bundler:Gemfile(-.*)?\\.lock"},
   432  			},
   433  			want: &analyzer.AnalysisResult{
   434  				Applications: []types.Application{
   435  					{
   436  						Type:     "bundler",
   437  						FilePath: "/app/Gemfile-dev.lock",
   438  						Libraries: types.Packages{
   439  							{
   440  								ID:       "actioncable@5.2.3",
   441  								Name:     "actioncable",
   442  								Version:  "5.2.3",
   443  								Indirect: false,
   444  								DependsOn: []string{
   445  									"actionpack@5.2.3",
   446  								},
   447  								Locations: []types.Location{
   448  									{
   449  										StartLine: 4,
   450  										EndLine:   4,
   451  									},
   452  								},
   453  							},
   454  							{
   455  								ID:       "actionpack@5.2.3",
   456  								Name:     "actionpack",
   457  								Version:  "5.2.3",
   458  								Indirect: true,
   459  								Locations: []types.Location{
   460  									{
   461  										StartLine: 6,
   462  										EndLine:   6,
   463  									},
   464  								},
   465  							},
   466  						},
   467  					},
   468  				},
   469  			},
   470  		},
   471  		{
   472  			name: "ignore permission error",
   473  			args: args{
   474  				filePath:     "/etc/alpine-release",
   475  				testFilePath: "testdata/no-permission",
   476  			},
   477  			want: &analyzer.AnalysisResult{},
   478  		},
   479  		{
   480  			name: "sad path with opener error",
   481  			args: args{
   482  				filePath:     "/lib/apk/db/installed",
   483  				testFilePath: "testdata/error",
   484  			},
   485  			wantErr: "unable to open /lib/apk/db/installed",
   486  		},
   487  		{
   488  			name: "sad path with broken file pattern regex",
   489  			args: args{
   490  				filePath:     "/app/Gemfile-dev.lock",
   491  				testFilePath: "testdata/app/Gemfile.lock",
   492  				filePatterns: []string{"bundler:Gemfile(-.*?\\.lock"},
   493  			},
   494  			wantErr: "error parsing regexp",
   495  		},
   496  		{
   497  			name: "sad path with broken file pattern",
   498  			args: args{
   499  				filePath:     "/app/Gemfile-dev.lock",
   500  				testFilePath: "testdata/app/Gemfile.lock",
   501  				filePatterns: []string{"Gemfile(-.*)?\\.lock"},
   502  			},
   503  			wantErr: "invalid file pattern",
   504  		},
   505  	}
   506  	for _, tt := range tests {
   507  		t.Run(tt.name, func(t *testing.T) {
   508  			var wg sync.WaitGroup
   509  			limit := semaphore.NewWeighted(3)
   510  
   511  			got := new(analyzer.AnalysisResult)
   512  			a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{
   513  				FilePatterns:      tt.args.filePatterns,
   514  				DisabledAnalyzers: tt.args.disabledAnalyzers,
   515  			})
   516  			if err != nil && tt.wantErr != "" {
   517  				require.NotNil(t, err)
   518  				assert.Contains(t, err.Error(), tt.wantErr)
   519  				return
   520  			}
   521  			require.NoError(t, err)
   522  
   523  			info, err := os.Stat(tt.args.testFilePath)
   524  			require.NoError(t, err)
   525  
   526  			ctx := context.Background()
   527  			err = a.AnalyzeFile(ctx, &wg, limit, got, "", tt.args.filePath, info,
   528  				func() (dio.ReadSeekCloserAt, error) {
   529  					if tt.args.testFilePath == "testdata/error" {
   530  						return nil, xerrors.New("error")
   531  					} else if tt.args.testFilePath == "testdata/no-permission" {
   532  						os.Chmod(tt.args.testFilePath, 0000)
   533  						t.Cleanup(func() {
   534  							os.Chmod(tt.args.testFilePath, 0644)
   535  						})
   536  					}
   537  					return os.Open(tt.args.testFilePath)
   538  				},
   539  				nil, analyzer.AnalysisOptions{},
   540  			)
   541  
   542  			wg.Wait()
   543  			if tt.wantErr != "" {
   544  				require.NotNil(t, err)
   545  				assert.Contains(t, err.Error(), tt.wantErr)
   546  				return
   547  			}
   548  
   549  			require.NoError(t, err)
   550  			assert.Equal(t, tt.want, got)
   551  		})
   552  	}
   553  }
   554  
   555  func TestAnalyzerGroup_PostAnalyze(t *testing.T) {
   556  	tests := []struct {
   557  		name         string
   558  		dir          string
   559  		analyzerType analyzer.Type
   560  		want         *analyzer.AnalysisResult
   561  	}{
   562  		{
   563  			name:         "jars with invalid jar",
   564  			dir:          "testdata/post-apps/jar/",
   565  			analyzerType: analyzer.TypeJar,
   566  			want: &analyzer.AnalysisResult{
   567  				Applications: []types.Application{
   568  					{
   569  						Type:     types.Jar,
   570  						FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar",
   571  						Libraries: types.Packages{
   572  							{
   573  								Name:     "com.fasterxml.jackson.core:jackson-annotations",
   574  								Version:  "2.15.0-rc2",
   575  								FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar",
   576  							},
   577  						},
   578  					},
   579  				},
   580  			},
   581  		},
   582  		{
   583  			name:         "poetry files with invalid file",
   584  			dir:          "testdata/post-apps/poetry/",
   585  			analyzerType: analyzer.TypePoetry,
   586  			want: &analyzer.AnalysisResult{
   587  				Applications: []types.Application{
   588  					{
   589  						Type:     types.Poetry,
   590  						FilePath: "testdata/post-apps/poetry/happy/poetry.lock",
   591  						Libraries: types.Packages{
   592  							{
   593  								ID:      "certifi@2022.12.7",
   594  								Name:    "certifi",
   595  								Version: "2022.12.7",
   596  							},
   597  						},
   598  					},
   599  				},
   600  			},
   601  		},
   602  	}
   603  	for _, tt := range tests {
   604  		t.Run(tt.name, func(t *testing.T) {
   605  			a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{})
   606  			require.NoError(t, err)
   607  
   608  			// Create a virtual filesystem
   609  			composite, err := analyzer.NewCompositeFS(analyzer.AnalyzerGroup{})
   610  			require.NoError(t, err)
   611  
   612  			mfs := mapfs.New()
   613  			require.NoError(t, mfs.CopyFilesUnder(tt.dir))
   614  			composite.Set(tt.analyzerType, mfs)
   615  
   616  			if tt.analyzerType == analyzer.TypeJar {
   617  				// init java-trivy-db with skip update
   618  				javadb.Init("./language/java/jar/testdata", "ghcr.io/aquasecurity/trivy-java-db", true, false, types.RegistryOptions{Insecure: false})
   619  			}
   620  
   621  			ctx := context.Background()
   622  			got := new(analyzer.AnalysisResult)
   623  			err = a.PostAnalyze(ctx, composite, got, analyzer.AnalysisOptions{})
   624  			require.NoError(t, err)
   625  			assert.Equal(t, tt.want, got)
   626  		})
   627  	}
   628  }
   629  
   630  func TestAnalyzerGroup_AnalyzerVersions(t *testing.T) {
   631  	tests := []struct {
   632  		name     string
   633  		disabled []analyzer.Type
   634  		want     analyzer.Versions
   635  	}{
   636  		{
   637  			name:     "happy path",
   638  			disabled: []analyzer.Type{},
   639  			want: analyzer.Versions{
   640  				Analyzers: map[string]int{
   641  					"alpine":     1,
   642  					"apk-repo":   1,
   643  					"apk":        2,
   644  					"bundler":    1,
   645  					"ubuntu":     1,
   646  					"ubuntu-esm": 1,
   647  				},
   648  				PostAnalyzers: map[string]int{
   649  					"jar":    1,
   650  					"poetry": 1,
   651  				},
   652  			},
   653  		},
   654  		{
   655  			name: "disable analyzers",
   656  			disabled: []analyzer.Type{
   657  				analyzer.TypeAlpine,
   658  				analyzer.TypeApkRepo,
   659  				analyzer.TypeUbuntu,
   660  				analyzer.TypeUbuntuESM,
   661  				analyzer.TypeJar,
   662  			},
   663  			want: analyzer.Versions{
   664  				Analyzers: map[string]int{
   665  					"apk":     2,
   666  					"bundler": 1,
   667  				},
   668  				PostAnalyzers: map[string]int{
   669  					"poetry": 1,
   670  				},
   671  			},
   672  		},
   673  	}
   674  	for _, tt := range tests {
   675  		t.Run(tt.name, func(t *testing.T) {
   676  			a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{
   677  				DisabledAnalyzers: tt.disabled,
   678  			})
   679  			require.NoError(t, err)
   680  			got := a.AnalyzerVersions()
   681  			fmt.Printf("%v\n", got)
   682  			assert.Equal(t, tt.want, got)
   683  		})
   684  	}
   685  }