github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/internal/fileresolver/container_image_all_layers_test.go (about) 1 package fileresolver 2 3 import ( 4 "io" 5 "sort" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/scylladb/go-set/strset" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/anchore/stereoscope/pkg/imagetest" 14 "github.com/anchore/syft/syft/file" 15 ) 16 17 type resolution struct { 18 layer uint 19 path string 20 } 21 22 func TestAllLayersResolver_FilesByPath(t *testing.T) { 23 cases := []struct { 24 name string 25 linkPath string 26 resolutions []resolution 27 forcePositiveHasPath bool 28 }{ 29 { 30 name: "link with previous data", 31 linkPath: "/link-1", 32 resolutions: []resolution{ 33 { 34 layer: 1, 35 path: "/file-1.txt", 36 }, 37 }, 38 }, 39 { 40 name: "link with in layer data", 41 linkPath: "/link-within", 42 resolutions: []resolution{ 43 { 44 layer: 5, 45 path: "/file-3.txt", 46 }, 47 }, 48 }, 49 { 50 name: "link with overridden data", 51 linkPath: "/link-2", 52 resolutions: []resolution{ 53 { 54 layer: 4, 55 path: "/file-2.txt", 56 }, 57 { 58 layer: 7, 59 path: "/file-2.txt", 60 }, 61 }, 62 }, 63 { 64 name: "indirect link (with overridden data)", 65 linkPath: "/link-indirect", 66 resolutions: []resolution{ 67 { 68 layer: 4, 69 path: "/file-2.txt", 70 }, 71 { 72 layer: 7, 73 path: "/file-2.txt", 74 }, 75 }, 76 }, 77 { 78 name: "dead link", 79 linkPath: "/link-dead", 80 resolutions: []resolution{}, 81 forcePositiveHasPath: true, 82 }, 83 { 84 name: "ignore directories", 85 linkPath: "/bin", 86 resolutions: []resolution{}, 87 // directories don't resolve BUT do exist 88 forcePositiveHasPath: true, 89 }, 90 } 91 for _, c := range cases { 92 t.Run(c.name, func(t *testing.T) { 93 img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks") 94 95 resolver, err := NewFromContainerImageAllLayers(img) 96 require.NoError(t, err) 97 98 hasPath := resolver.HasPath(c.linkPath) 99 if !c.forcePositiveHasPath { 100 if len(c.resolutions) > 0 && !hasPath { 101 t.Errorf("expected HasPath() to indicate existance, but did not") 102 } else if len(c.resolutions) == 0 && hasPath { 103 t.Errorf("expeced HasPath() to NOT indicate existance, but does") 104 } 105 } else if !hasPath { 106 t.Errorf("expected HasPath() to indicate existance, but did not (force path)") 107 } 108 109 refs, err := resolver.FilesByPath(c.linkPath) 110 require.NoError(t, err) 111 112 if len(refs) != len(c.resolutions) { 113 t.Fatalf("unexpected number of resolutions: %d", len(refs)) 114 } 115 116 for idx, actual := range refs { 117 expected := c.resolutions[idx] 118 119 if string(actual.Reference().RealPath) != expected.path { 120 t.Errorf("bad resolve path: '%s'!='%s'", string(actual.Reference().RealPath), expected.path) 121 } 122 123 if expected.path != "" && string(actual.Reference().RealPath) != actual.RealPath { 124 t.Errorf("we should always prefer real paths over ones with links") 125 } 126 127 layer := img.FileCatalog.Layer(actual.Reference()) 128 if layer.Metadata.Index != expected.layer { 129 t.Errorf("bad resolve layer: '%d'!='%d'", layer.Metadata.Index, expected.layer) 130 } 131 } 132 }) 133 } 134 } 135 136 func TestAllLayersResolver_FilesByGlob(t *testing.T) { 137 cases := []struct { 138 name string 139 glob string 140 resolutions []resolution 141 }{ 142 { 143 name: "link with previous data", 144 glob: "**/*ink-1", 145 resolutions: []resolution{ 146 { 147 layer: 1, 148 path: "/file-1.txt", 149 }, 150 }, 151 }, 152 { 153 name: "link with in layer data", 154 glob: "**/*nk-within", 155 resolutions: []resolution{ 156 { 157 layer: 5, 158 path: "/file-3.txt", 159 }, 160 }, 161 }, 162 { 163 name: "link with overridden data", 164 glob: "**/*ink-2", 165 resolutions: []resolution{ 166 { 167 layer: 4, 168 path: "/file-2.txt", 169 }, 170 { 171 layer: 7, 172 path: "/file-2.txt", 173 }, 174 }, 175 }, 176 { 177 name: "indirect link (with overridden data)", 178 glob: "**/*nk-indirect", 179 resolutions: []resolution{ 180 { 181 layer: 4, 182 path: "/file-2.txt", 183 }, 184 { 185 layer: 7, 186 path: "/file-2.txt", 187 }, 188 }, 189 }, 190 { 191 name: "dead link", 192 glob: "**/*k-dead", 193 resolutions: []resolution{}, 194 }, 195 { 196 name: "ignore directories", 197 glob: "**/bin", 198 resolutions: []resolution{}, 199 }, 200 } 201 for _, c := range cases { 202 t.Run(c.name, func(t *testing.T) { 203 img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks") 204 205 resolver, err := NewFromContainerImageAllLayers(img) 206 require.NoError(t, err) 207 208 refs, err := resolver.FilesByGlob(c.glob) 209 require.NoError(t, err) 210 211 if len(refs) != len(c.resolutions) { 212 t.Fatalf("unexpected number of resolutions: %d", len(refs)) 213 } 214 215 for idx, actual := range refs { 216 expected := c.resolutions[idx] 217 218 if string(actual.Reference().RealPath) != expected.path { 219 t.Errorf("bad resolve path: '%s'!='%s'", string(actual.Reference().RealPath), expected.path) 220 } 221 222 if expected.path != "" && string(actual.Reference().RealPath) != actual.RealPath { 223 t.Errorf("we should always prefer real paths over ones with links") 224 } 225 226 layer := img.FileCatalog.Layer(actual.Reference()) 227 228 if layer.Metadata.Index != expected.layer { 229 t.Errorf("bad resolve layer: '%d'!='%d'", layer.Metadata.Index, expected.layer) 230 } 231 } 232 }) 233 } 234 } 235 236 func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) { 237 238 tests := []struct { 239 fixtureName string 240 mimeType string 241 expectedPaths []string 242 }{ 243 { 244 fixtureName: "image-duplicate-path", 245 mimeType: "text/plain", 246 expectedPaths: []string{"/somefile-1.txt", "/somefile-1.txt"}, 247 }, 248 } 249 for _, test := range tests { 250 t.Run(test.fixtureName, func(t *testing.T) { 251 img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName) 252 253 resolver, err := NewFromContainerImageAllLayers(img) 254 assert.NoError(t, err) 255 256 locations, err := resolver.FilesByMIMEType(test.mimeType) 257 assert.NoError(t, err) 258 259 assert.Len(t, test.expectedPaths, len(locations)) 260 for idx, l := range locations { 261 assert.Equal(t, test.expectedPaths[idx], l.RealPath, "does not have path %q", l.RealPath) 262 } 263 }) 264 } 265 } 266 267 func Test_imageAllLayersResolver_hasFilesystemIDInLocation(t *testing.T) { 268 img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path") 269 270 resolver, err := NewFromContainerImageAllLayers(img) 271 assert.NoError(t, err) 272 273 locations, err := resolver.FilesByMIMEType("text/plain") 274 assert.NoError(t, err) 275 assert.NotEmpty(t, locations) 276 for _, location := range locations { 277 assert.NotEmpty(t, location.FileSystemID) 278 } 279 280 locations, err = resolver.FilesByGlob("*.txt") 281 assert.NoError(t, err) 282 assert.NotEmpty(t, locations) 283 for _, location := range locations { 284 assert.NotEmpty(t, location.FileSystemID) 285 } 286 287 locations, err = resolver.FilesByPath("/somefile-1.txt") 288 assert.NoError(t, err) 289 assert.NotEmpty(t, locations) 290 for _, location := range locations { 291 assert.NotEmpty(t, location.FileSystemID) 292 } 293 294 } 295 296 func TestAllLayersImageResolver_FilesContents(t *testing.T) { 297 298 tests := []struct { 299 name string 300 fixture string 301 contents []string 302 }{ 303 { 304 name: "one degree", 305 fixture: "link-2", 306 contents: []string{ 307 "file 2!", // from the first resolved layer's perspective 308 "NEW file override!", // from the second resolved layers perspective 309 }, 310 }, 311 { 312 name: "two degrees", 313 fixture: "link-indirect", 314 contents: []string{ 315 "file 2!", 316 "NEW file override!", 317 }, 318 }, 319 { 320 name: "dead link", 321 fixture: "link-dead", 322 contents: []string{}, 323 }, 324 } 325 326 for _, test := range tests { 327 t.Run(test.name, func(t *testing.T) { 328 img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks") 329 330 resolver, err := NewFromContainerImageAllLayers(img) 331 assert.NoError(t, err) 332 333 refs, err := resolver.FilesByPath(test.fixture) 334 require.NoError(t, err) 335 336 // the given path should have an overridden file 337 require.Len(t, refs, len(test.contents)) 338 339 for idx, loc := range refs { 340 reader, err := resolver.FileContentsByLocation(loc) 341 require.NoError(t, err) 342 343 actual, err := io.ReadAll(reader) 344 require.NoError(t, err) 345 346 assert.Equal(t, test.contents[idx], string(actual)) 347 } 348 349 }) 350 } 351 } 352 353 func TestAllLayersImageResolver_FilesContents_errorOnDirRequest(t *testing.T) { 354 355 img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks") 356 357 resolver, err := NewFromContainerImageAllLayers(img) 358 assert.NoError(t, err) 359 360 var dirLoc *file.Location 361 for loc := range resolver.AllLocations() { 362 entry, err := resolver.img.FileCatalog.Get(loc.Reference()) 363 require.NoError(t, err) 364 if entry.Metadata.IsDir() { 365 dirLoc = &loc 366 break 367 } 368 } 369 370 require.NotNil(t, dirLoc) 371 372 reader, err := resolver.FileContentsByLocation(*dirLoc) 373 require.Error(t, err) 374 require.Nil(t, reader) 375 } 376 377 func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) { 378 tests := []struct { 379 name string 380 runner func(file.Resolver) []file.Location 381 expected []file.Location 382 }{ 383 { 384 name: "by mimetype", 385 runner: func(resolver file.Resolver) []file.Location { 386 // links should not show up when searching mimetype 387 actualLocations, err := resolver.FilesByMIMEType("text/plain") 388 assert.NoError(t, err) 389 return actualLocations 390 }, 391 expected: []file.Location{ 392 file.NewVirtualLocation("/etc/group", "/etc/group"), 393 file.NewVirtualLocation("/etc/passwd", "/etc/passwd"), 394 file.NewVirtualLocation("/etc/shadow", "/etc/shadow"), 395 file.NewVirtualLocation("/file-1.txt", "/file-1.txt"), 396 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1 397 // note: we're de-duping the redundant access to file-3.txt 398 // ... (there would usually be two copies) 399 file.NewVirtualLocation("/file-3.txt", "/file-3.txt"), 400 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2 401 file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // copy 1 402 file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // copy 2 403 }, 404 }, 405 { 406 name: "by glob to links", 407 runner: func(resolver file.Resolver) []file.Location { 408 // links are searched, but resolve to the real files 409 actualLocations, err := resolver.FilesByGlob("*ink-*") 410 assert.NoError(t, err) 411 return actualLocations 412 }, 413 expected: []file.Location{ 414 file.NewVirtualLocation("/file-1.txt", "/link-1"), 415 file.NewVirtualLocation("/file-2.txt", "/link-2"), // copy 1 416 file.NewVirtualLocation("/file-2.txt", "/link-2"), // copy 2 417 file.NewVirtualLocation("/file-3.txt", "/link-within"), 418 }, 419 }, 420 { 421 name: "by basename", 422 runner: func(resolver file.Resolver) []file.Location { 423 // links are searched, but resolve to the real files 424 actualLocations, err := resolver.FilesByGlob("**/file-2.txt") 425 assert.NoError(t, err) 426 return actualLocations 427 }, 428 expected: []file.Location{ 429 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1 430 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2 431 }, 432 }, 433 { 434 name: "by basename glob", 435 runner: func(resolver file.Resolver) []file.Location { 436 // links are searched, but resolve to the real files 437 actualLocations, err := resolver.FilesByGlob("**/file-?.txt") 438 assert.NoError(t, err) 439 return actualLocations 440 }, 441 expected: []file.Location{ 442 file.NewVirtualLocation("/file-1.txt", "/file-1.txt"), 443 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1 444 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2 445 file.NewVirtualLocation("/file-3.txt", "/file-3.txt"), 446 file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), 447 file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // when we copy into the link path, the same file-4.txt is copied 448 }, 449 }, 450 { 451 name: "by extension", 452 runner: func(resolver file.Resolver) []file.Location { 453 // links are searched, but resolve to the real files 454 actualLocations, err := resolver.FilesByGlob("**/*.txt") 455 assert.NoError(t, err) 456 return actualLocations 457 }, 458 expected: []file.Location{ 459 file.NewVirtualLocation("/file-1.txt", "/file-1.txt"), 460 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1 461 file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2 462 file.NewVirtualLocation("/file-3.txt", "/file-3.txt"), 463 file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), 464 file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // when we copy into the link path, the same file-4.txt is copied 465 }, 466 }, 467 { 468 name: "by path to degree 1 link", 469 runner: func(resolver file.Resolver) []file.Location { 470 // links resolve to the final file 471 actualLocations, err := resolver.FilesByPath("/link-2") 472 assert.NoError(t, err) 473 return actualLocations 474 }, 475 expected: []file.Location{ 476 // we have multiple copies across layers 477 file.NewVirtualLocation("/file-2.txt", "/link-2"), 478 file.NewVirtualLocation("/file-2.txt", "/link-2"), 479 }, 480 }, 481 { 482 name: "by path to degree 2 link", 483 runner: func(resolver file.Resolver) []file.Location { 484 // multiple links resolves to the final file 485 actualLocations, err := resolver.FilesByPath("/link-indirect") 486 assert.NoError(t, err) 487 return actualLocations 488 }, 489 expected: []file.Location{ 490 // we have multiple copies across layers 491 file.NewVirtualLocation("/file-2.txt", "/link-indirect"), 492 file.NewVirtualLocation("/file-2.txt", "/link-indirect"), 493 }, 494 }, 495 } 496 497 for _, test := range tests { 498 t.Run(test.name, func(t *testing.T) { 499 500 img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks") 501 502 resolver, err := NewFromContainerImageAllLayers(img) 503 assert.NoError(t, err) 504 505 actual := test.runner(resolver) 506 507 compareLocations(t, test.expected, actual) 508 }) 509 } 510 511 } 512 513 func TestAllLayersResolver_AllLocations(t *testing.T) { 514 img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted") 515 516 resolver, err := NewFromContainerImageAllLayers(img) 517 assert.NoError(t, err) 518 519 paths := strset.New() 520 for loc := range resolver.AllLocations() { 521 paths.Add(loc.RealPath) 522 } 523 expected := []string{ 524 "/Dockerfile", 525 "/file-1.txt", 526 "/file-3.txt", 527 "/target", 528 "/target/file-2.txt", 529 530 "/.wh.bin", 531 "/.wh.file-1.txt", 532 "/.wh.lib", 533 "/bin", 534 "/bin/arch", 535 "/bin/ash", 536 "/bin/base64", 537 "/bin/bbconfig", 538 "/bin/busybox", 539 "/bin/cat", 540 "/bin/chattr", 541 "/bin/chgrp", 542 "/bin/chmod", 543 "/bin/chown", 544 "/bin/cp", 545 "/bin/date", 546 "/bin/dd", 547 "/bin/df", 548 "/bin/dmesg", 549 "/bin/dnsdomainname", 550 "/bin/dumpkmap", 551 "/bin/echo", 552 "/bin/ed", 553 "/bin/egrep", 554 "/bin/false", 555 "/bin/fatattr", 556 "/bin/fdflush", 557 "/bin/fgrep", 558 "/bin/fsync", 559 "/bin/getopt", 560 "/bin/grep", 561 "/bin/gunzip", 562 "/bin/gzip", 563 "/bin/hostname", 564 "/bin/ionice", 565 "/bin/iostat", 566 "/bin/ipcalc", 567 "/bin/kbd_mode", 568 "/bin/kill", 569 "/bin/link", 570 "/bin/linux32", 571 "/bin/linux64", 572 "/bin/ln", 573 "/bin/login", 574 "/bin/ls", 575 "/bin/lsattr", 576 "/bin/lzop", 577 "/bin/makemime", 578 "/bin/mkdir", 579 "/bin/mknod", 580 "/bin/mktemp", 581 "/bin/more", 582 "/bin/mount", 583 "/bin/mountpoint", 584 "/bin/mpstat", 585 "/bin/mv", 586 "/bin/netstat", 587 "/bin/nice", 588 "/bin/pidof", 589 "/bin/ping", 590 "/bin/ping6", 591 "/bin/pipe_progress", 592 "/bin/printenv", 593 "/bin/ps", 594 "/bin/pwd", 595 "/bin/reformime", 596 "/bin/rev", 597 "/bin/rm", 598 "/bin/rmdir", 599 "/bin/run-parts", 600 "/bin/sed", 601 "/bin/setpriv", 602 "/bin/setserial", 603 "/bin/sh", 604 "/bin/sleep", 605 "/bin/stat", 606 "/bin/stty", 607 "/bin/su", 608 "/bin/sync", 609 "/bin/tar", 610 "/bin/touch", 611 "/bin/true", 612 "/bin/umount", 613 "/bin/uname", 614 "/bin/usleep", 615 "/bin/watch", 616 "/bin/zcat", 617 "/lib", 618 "/lib/apk", 619 "/lib/apk/db", 620 "/lib/apk/db/installed", 621 "/lib/apk/db/lock", 622 "/lib/apk/db/scripts.tar", 623 "/lib/apk/db/triggers", 624 "/lib/apk/exec", 625 "/lib/firmware", 626 "/lib/ld-musl-x86_64.so.1", 627 "/lib/libapk.so.3.12.0", 628 "/lib/libc.musl-x86_64.so.1", 629 "/lib/libcrypto.so.3", 630 "/lib/libssl.so.3", 631 "/lib/libz.so.1", 632 "/lib/libz.so.1.2.13", 633 "/lib/mdev", 634 "/lib/modules-load.d", 635 "/lib/sysctl.d", 636 "/lib/sysctl.d/00-alpine.conf", 637 } 638 639 // depending on how the image is built (either from linux or mac), sys and proc might accidentally be added to the image. 640 // this isn't important for the test, so we remove them. 641 paths.Remove("/proc", "/sys", "/dev", "/etc") 642 643 pathsList := paths.List() 644 sort.Strings(pathsList) 645 646 assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List())) 647 }