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 }