github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/test/integration/library_test.go (about)

     1  //go:build integration
     2  // +build integration
     3  
     4  package integration
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"flag"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"sort"
    14  	"strings"
    15  	"testing"
    16  
    17  	dtypes "github.com/docker/docker/api/types"
    18  	"github.com/docker/docker/client"
    19  	"github.com/stretchr/testify/assert"
    20  	"github.com/stretchr/testify/require"
    21  
    22  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    23  
    24  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/all"
    25  	"github.com/devseccon/trivy/pkg/fanal/applier"
    26  	"github.com/devseccon/trivy/pkg/fanal/artifact"
    27  	aimage "github.com/devseccon/trivy/pkg/fanal/artifact/image"
    28  	"github.com/devseccon/trivy/pkg/fanal/cache"
    29  	_ "github.com/devseccon/trivy/pkg/fanal/handler/all"
    30  	"github.com/devseccon/trivy/pkg/fanal/image"
    31  	"github.com/devseccon/trivy/pkg/fanal/types"
    32  
    33  	_ "modernc.org/sqlite"
    34  )
    35  
    36  var update = flag.Bool("update", false, "update golden files")
    37  
    38  type testCase struct {
    39  	name                string
    40  	remoteImageName     string
    41  	imageFile           string
    42  	wantOS              types.OS
    43  	wantPkgsFromCmds    string
    44  	wantApplicationFile string
    45  }
    46  
    47  var tests = []testCase{
    48  	{
    49  		name:            "happy path, alpine:3.10",
    50  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:alpine-310",
    51  		imageFile:       "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz",
    52  		wantOS: types.OS{
    53  			Name:   "3.10.2",
    54  			Family: "alpine",
    55  		},
    56  	},
    57  	{
    58  		name:            "happy path, amazonlinux:2",
    59  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:amazon-2",
    60  		imageFile:       "../../../../integration/testdata/fixtures/images/amazon-2.tar.gz",
    61  		wantOS: types.OS{
    62  			Name:   "2 (Karoo)",
    63  			Family: "amazon",
    64  		},
    65  	},
    66  	{
    67  		name:            "happy path, debian:buster",
    68  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:debian-buster",
    69  		imageFile:       "../../../../integration/testdata/fixtures/images/debian-buster.tar.gz",
    70  		wantOS: types.OS{
    71  			Name:   "10.1",
    72  			Family: "debian",
    73  		},
    74  	},
    75  	{
    76  		name:            "happy path, photon:3.0",
    77  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:photon-30",
    78  		imageFile:       "../../../../integration/testdata/fixtures/images/photon-30.tar.gz",
    79  		wantOS: types.OS{
    80  			Name:   "3.0",
    81  			Family: "photon",
    82  		},
    83  	},
    84  	{
    85  		name:            "happy path, registry.redhat.io/ubi7",
    86  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:ubi-7",
    87  		imageFile:       "../../../../integration/testdata/fixtures/images/ubi-7.tar.gz",
    88  		wantOS: types.OS{
    89  			Name:   "7.7",
    90  			Family: "redhat",
    91  		},
    92  	},
    93  	{
    94  		name:            "happy path, opensuse leap 15.1",
    95  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:opensuse-leap-151",
    96  		imageFile:       "../../../../integration/testdata/fixtures/images/opensuse-leap-151.tar.gz",
    97  		wantOS: types.OS{
    98  			Name:   "15.1",
    99  			Family: "opensuse.leap",
   100  		},
   101  	},
   102  	{
   103  		// from registry.suse.com/suse/sle15:15.3.17.8.16
   104  		name:            "happy path, suse 15.3 (NDB)",
   105  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:suse-15.3_ndb",
   106  		imageFile:       "../../../../integration/testdata/fixtures/images/suse-15.3_ndb.tar.gz",
   107  		wantOS: types.OS{
   108  			Name:   "15.3",
   109  			Family: "suse linux enterprise server",
   110  		},
   111  	},
   112  	{
   113  		name:            "happy path, Fedora 35",
   114  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:fedora-35",
   115  		imageFile:       "../../../../integration/testdata/fixtures/images/fedora-35.tar.gz",
   116  		wantOS: types.OS{
   117  			Name:   "35",
   118  			Family: "fedora",
   119  		},
   120  	},
   121  	{
   122  		name:            "happy path, vulnimage with lock files",
   123  		remoteImageName: "ghcr.io/devseccon/trivy-test-images:vulnimage",
   124  		imageFile:       "../../../../integration/testdata/fixtures/images/vulnimage.tar.gz",
   125  		wantOS: types.OS{
   126  			Name:   "3.7.1",
   127  			Family: "alpine",
   128  		},
   129  		wantApplicationFile: "testdata/goldens/vuln-image1.2.3.expectedlibs.golden",
   130  		wantPkgsFromCmds:    "testdata/goldens/vuln-image1.2.3.expectedpkgsfromcmds.golden",
   131  	},
   132  }
   133  
   134  func TestFanal_Library_DockerLessMode(t *testing.T) {
   135  	for _, tt := range tests {
   136  		t.Run(tt.name, func(t *testing.T) {
   137  			t.Parallel()
   138  			ctx := context.Background()
   139  			d := t.TempDir()
   140  
   141  			c, err := cache.NewFSCache(d)
   142  			require.NoError(t, err, tt.name)
   143  
   144  			cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
   145  			require.NoError(t, err)
   146  
   147  			// remove existing Image if any
   148  			_, _ = cli.ImageRemove(ctx, tt.remoteImageName, dtypes.ImageRemoveOptions{
   149  				Force:         true,
   150  				PruneChildren: true,
   151  			})
   152  
   153  			// Enable only registry scanning
   154  			img, cleanup, err := image.NewContainerImage(ctx, tt.remoteImageName, types.ImageOptions{
   155  				ImageSources: types.ImageSources{types.RemoteImageSource},
   156  			})
   157  			require.NoError(t, err)
   158  			defer cleanup()
   159  
   160  			// don't scan licenses in the test - in parallel it will fail
   161  			ar, err := aimage.NewArtifact(img, c, artifact.Option{
   162  				DisabledAnalyzers: []analyzer.Type{
   163  					analyzer.TypeExecutable,
   164  					analyzer.TypeLicenseFile,
   165  				},
   166  			})
   167  			require.NoError(t, err)
   168  
   169  			applier := applier.NewApplier(c)
   170  
   171  			// run tests twice, one without cache and with cache
   172  			for i := 1; i <= 2; i++ {
   173  				runChecks(t, ctx, ar, applier, tt)
   174  			}
   175  
   176  			// clear Cache
   177  			require.NoError(t, c.Clear())
   178  		})
   179  	}
   180  }
   181  
   182  func TestFanal_Library_DockerMode(t *testing.T) {
   183  	// Disable updating golden files because local images don't have compressed layer digests,
   184  	// and updating golden files in this function results in incomplete files.
   185  	if *update {
   186  		t.Skipf("This test creates wrong golden file")
   187  	}
   188  	for _, tt := range tests {
   189  		t.Run(tt.name, func(t *testing.T) {
   190  			ctx := context.Background()
   191  			d := t.TempDir()
   192  
   193  			c, err := cache.NewFSCache(d)
   194  			require.NoError(t, err)
   195  
   196  			cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
   197  			require.NoError(t, err, tt.name)
   198  
   199  			testfile, err := os.Open(tt.imageFile)
   200  			require.NoError(t, err)
   201  
   202  			// load image into docker engine
   203  			resp, err := cli.ImageLoad(ctx, testfile, true)
   204  			require.NoError(t, err, tt.name)
   205  			_, err = io.Copy(io.Discard, resp.Body)
   206  			require.NoError(t, err, tt.name)
   207  
   208  			// Enable only dockerd scanning
   209  			img, cleanup, err := image.NewContainerImage(ctx, tt.remoteImageName, types.ImageOptions{
   210  				ImageSources: types.ImageSources{types.DockerImageSource},
   211  			})
   212  			require.NoError(t, err, tt.name)
   213  			defer cleanup()
   214  
   215  			ar, err := aimage.NewArtifact(img, c, artifact.Option{
   216  				// disable license checking in the test - in parallel it will fail because of resource requirement
   217  				DisabledAnalyzers: []analyzer.Type{
   218  					analyzer.TypeExecutable,
   219  					analyzer.TypeLicenseFile,
   220  				},
   221  			})
   222  			require.NoError(t, err)
   223  
   224  			applier := applier.NewApplier(c)
   225  
   226  			// run tests twice, one without cache and with cache
   227  			for i := 1; i <= 2; i++ {
   228  				runChecks(t, ctx, ar, applier, tt)
   229  			}
   230  
   231  			// clear Cache
   232  			require.NoError(t, c.Clear(), tt.name)
   233  
   234  			_, _ = cli.ImageRemove(ctx, tt.remoteImageName, dtypes.ImageRemoveOptions{
   235  				Force:         true,
   236  				PruneChildren: true,
   237  			})
   238  		})
   239  	}
   240  }
   241  
   242  func TestFanal_Library_TarMode(t *testing.T) {
   243  	for _, tt := range tests {
   244  		t.Run(tt.name, func(t *testing.T) {
   245  			t.Parallel()
   246  			ctx := context.Background()
   247  			d := t.TempDir()
   248  
   249  			c, err := cache.NewFSCache(d)
   250  			require.NoError(t, err)
   251  
   252  			img, err := image.NewArchiveImage(tt.imageFile)
   253  			require.NoError(t, err, tt.name)
   254  
   255  			ar, err := aimage.NewArtifact(img, c, artifact.Option{
   256  				DisabledAnalyzers: []analyzer.Type{
   257  					analyzer.TypeExecutable,
   258  					analyzer.TypeLicenseFile,
   259  				},
   260  			})
   261  			require.NoError(t, err)
   262  
   263  			applier := applier.NewApplier(c)
   264  
   265  			runChecks(t, ctx, ar, applier, tt)
   266  
   267  			// clear Cache
   268  			require.NoError(t, c.Clear(), tt.name)
   269  		})
   270  	}
   271  }
   272  
   273  func runChecks(t *testing.T, ctx context.Context, ar artifact.Artifact, applier applier.Applier, tc testCase) {
   274  	imageInfo, err := ar.Inspect(ctx)
   275  	require.NoError(t, err, tc.name)
   276  	imageDetail, err := applier.ApplyLayers(imageInfo.ID, imageInfo.BlobIDs)
   277  	require.NoError(t, err, tc.name)
   278  	commonChecks(t, imageDetail, tc)
   279  }
   280  
   281  func commonChecks(t *testing.T, detail types.ArtifactDetail, tc testCase) {
   282  	assert.Equal(t, tc.wantOS, detail.OS, tc.name)
   283  	checkOSPackages(t, detail, tc)
   284  	checkPackageFromCommands(t, detail, tc)
   285  	checkLangPkgs(detail, t, tc)
   286  }
   287  
   288  func checkOSPackages(t *testing.T, detail types.ArtifactDetail, tc testCase) {
   289  	// Sort OS packages for consistency
   290  	sort.Sort(detail.Packages)
   291  
   292  	splitted := strings.Split(tc.remoteImageName, ":")
   293  	goldenFile := fmt.Sprintf("testdata/goldens/packages/%s.json.golden", splitted[len(splitted)-1])
   294  
   295  	if *update {
   296  		b, err := json.MarshalIndent(detail.Packages, "", "  ")
   297  		require.NoError(t, err)
   298  		err = os.WriteFile(goldenFile, b, 0666)
   299  		require.NoError(t, err)
   300  		return
   301  	}
   302  	data, err := os.ReadFile(goldenFile)
   303  	require.NoError(t, err, tc.name)
   304  
   305  	var expectedPkgs []types.Package
   306  	err = json.Unmarshal(data, &expectedPkgs)
   307  	require.NoError(t, err)
   308  
   309  	require.Equal(t, len(expectedPkgs), len(detail.Packages), tc.name)
   310  	sort.Slice(expectedPkgs, func(i, j int) bool { return expectedPkgs[i].Name < expectedPkgs[j].Name })
   311  	sort.Sort(detail.Packages)
   312  
   313  	for i := 0; i < len(expectedPkgs); i++ {
   314  		require.Equal(t, expectedPkgs[i].Name, detail.Packages[i].Name, tc.name)
   315  		require.Equal(t, expectedPkgs[i].Version, detail.Packages[i].Version, tc.name)
   316  	}
   317  }
   318  
   319  func checkLangPkgs(detail types.ArtifactDetail, t *testing.T, tc testCase) {
   320  	if tc.wantApplicationFile != "" {
   321  		// Sort applications for consistency
   322  		sort.Slice(detail.Applications, func(i, j int) bool {
   323  			if detail.Applications[i].Type != detail.Applications[j].Type {
   324  				return detail.Applications[i].Type < detail.Applications[j].Type
   325  			}
   326  			return detail.Applications[i].FilePath < detail.Applications[j].FilePath
   327  		})
   328  
   329  		for _, app := range detail.Applications {
   330  			sort.Sort(app.Libraries)
   331  			for i := range app.Libraries {
   332  				sort.Strings(app.Libraries[i].DependsOn)
   333  			}
   334  		}
   335  
   336  		// Do not compare layers
   337  		for _, app := range detail.Applications {
   338  			for i := range app.Libraries {
   339  				app.Libraries[i].Layer = types.Layer{}
   340  			}
   341  		}
   342  
   343  		if *update {
   344  			b, err := json.MarshalIndent(detail.Applications, "", "  ")
   345  			require.NoError(t, err)
   346  			err = os.WriteFile(tc.wantApplicationFile, b, 0666)
   347  			require.NoError(t, err)
   348  			return
   349  		}
   350  
   351  		var wantApps []types.Application
   352  		data, err := os.ReadFile(tc.wantApplicationFile)
   353  		require.NoError(t, err)
   354  		err = json.Unmarshal(data, &wantApps)
   355  		require.NoError(t, err)
   356  
   357  		require.Equal(t, wantApps, detail.Applications, tc.name)
   358  	} else {
   359  		assert.Nil(t, detail.Applications, tc.name)
   360  	}
   361  }
   362  
   363  func checkPackageFromCommands(t *testing.T, detail types.ArtifactDetail, tc testCase) {
   364  	if tc.wantPkgsFromCmds != "" {
   365  		data, _ := os.ReadFile(tc.wantPkgsFromCmds)
   366  		var expectedPkgsFromCmds []types.Package
   367  
   368  		err := json.Unmarshal(data, &expectedPkgsFromCmds)
   369  		require.NoError(t, err)
   370  		assert.ElementsMatch(t, expectedPkgsFromCmds, detail.ImageConfig.Packages, tc.name)
   371  	} else {
   372  		assert.Equal(t, []types.Package(nil), detail.ImageConfig.Packages, tc.name)
   373  	}
   374  }