syft/syft/source/source_test.go
2022-10-05 14:01:40 -04:00

797 lines
19 KiB
Go

//go:build !windows
// +build !windows
package source
import (
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/artifact"
)
func TestParseInput(t *testing.T) {
tests := []struct {
name string
input string
platform string
expected Scheme
errFn require.ErrorAssertionFunc
}{
{
name: "ParseInput parses a file input",
input: "test-fixtures/image-simple/file-1.txt",
expected: FileScheme,
},
{
name: "errors out when using platform for non-image scheme",
input: "test-fixtures/image-simple/file-1.txt",
platform: "arm64",
errFn: require.Error,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.errFn == nil {
test.errFn = require.NoError
}
sourceInput, err := ParseInput(test.input, test.platform, true)
test.errFn(t, err)
if test.expected != "" {
require.NotNil(t, sourceInput)
assert.Equal(t, sourceInput.Scheme, test.expected)
}
})
}
}
func TestNewFromImageFails(t *testing.T) {
t.Run("no image given", func(t *testing.T) {
_, err := NewFromImage(nil, "")
if err == nil {
t.Errorf("expected an error condition but none was given")
}
})
}
func TestSetID(t *testing.T) {
layer := image.NewLayer(nil)
layer.Metadata = image.LayerMetadata{
Digest: "sha256:6f4fb385d4e698647bf2a450749dfbb7bc2831ec9a730ef4046c78c08d468e89",
}
img := image.Image{
Layers: []*image.Layer{layer},
}
tests := []struct {
name string
input *Source
expected artifact.ID
}{
{
name: "source.SetID sets the ID for FileScheme",
input: &Source{
Metadata: Metadata{
Scheme: FileScheme,
Path: "test-fixtures/image-simple/file-1.txt",
},
},
expected: artifact.ID("55096713247489add592ce977637be868497132b36d1e294a3831925ec64319a"),
},
{
name: "source.SetID sets the ID for ImageScheme",
input: &Source{
Image: &img,
Metadata: Metadata{
Scheme: ImageScheme,
},
},
expected: artifact.ID("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
},
{
name: "source.SetID sets the ID for DirectoryScheme",
input: &Source{
Image: &img,
Metadata: Metadata{
Scheme: DirectoryScheme,
Path: "test-fixtures/image-simple",
},
},
expected: artifact.ID("91db61e5e0ae097ef764796ce85e442a93f2a03e5313d4c7307e9b413f62e8c4"),
},
{
name: "source.SetID sets the ID for UnknownScheme",
input: &Source{
Image: &img,
Metadata: Metadata{
Scheme: UnknownScheme,
Path: "test-fixtures/image-simple",
},
},
expected: artifact.ID("febd2d6148dc327d"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.input.SetID()
assert.Equal(t, test.expected, test.input.ID())
})
}
}
func TestNewFromImage(t *testing.T) {
layer := image.NewLayer(nil)
img := image.Image{
Layers: []*image.Layer{layer},
}
t.Run("create a new source object from image", func(t *testing.T) {
_, err := NewFromImage(&img, "")
if err != nil {
t.Errorf("unexpected error when creating a new Locations from img: %+v", err)
}
})
}
func TestNewFromDirectory(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
inputPaths []string
expectedRefs int
expectedErr bool
}{
{
desc: "no paths exist",
input: "foobar/",
inputPaths: []string{"/opt/", "/other"},
expectedErr: true,
},
{
desc: "path detected",
input: "test-fixtures",
inputPaths: []string{"path-detected/.vimrc"},
expectedRefs: 1,
},
{
desc: "directory ignored",
input: "test-fixtures",
inputPaths: []string{"path-detected"},
expectedRefs: 0,
},
{
desc: "no files-by-path detected",
input: "test-fixtures",
inputPaths: []string{"no-path-detected"},
expectedRefs: 0,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(test.input)
require.NoError(t, err)
assert.Equal(t, test.input, src.Metadata.Path)
resolver, err := src.FileResolver(SquashedScope)
if test.expectedErr {
if err == nil {
t.Fatal("expected an error when making the resolver but got none")
}
return
} else {
require.NoError(t, err)
}
refs, err := resolver.FilesByPath(test.inputPaths...)
if err != nil {
t.Errorf("FilesByPath call produced an error: %+v", err)
}
if len(refs) != test.expectedRefs {
t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expectedRefs)
}
})
}
}
func TestNewFromFile(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
inputPaths []string
expRefs int
}{
{
desc: "path detected",
input: "test-fixtures/path-detected",
inputPaths: []string{"/.vimrc"},
expRefs: 1,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, cleanup := NewFromFile(test.input)
if cleanup != nil {
t.Cleanup(cleanup)
}
assert.Equal(t, test.input, src.Metadata.Path)
assert.Equal(t, src.Metadata.Path, src.path)
resolver, err := src.FileResolver(SquashedScope)
require.NoError(t, err)
refs, err := resolver.FilesByPath(test.inputPaths...)
require.NoError(t, err)
assert.Len(t, refs, test.expRefs)
})
}
}
func TestNewFromFile_WithArchive(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
inputPaths []string
expRefs int
}{
{
desc: "path detected",
input: "test-fixtures/path-detected",
inputPaths: []string{"/.vimrc"},
expRefs: 1,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
archivePath := setupArchiveTest(t, test.input)
src, cleanup := NewFromFile(archivePath)
if cleanup != nil {
t.Cleanup(cleanup)
}
assert.Equal(t, archivePath, src.Metadata.Path)
assert.NotEqual(t, src.Metadata.Path, src.path)
resolver, err := src.FileResolver(SquashedScope)
require.NoError(t, err)
refs, err := resolver.FilesByPath(test.inputPaths...)
require.NoError(t, err)
assert.Len(t, refs, test.expRefs)
})
}
}
func TestNewFromDirectoryShared(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
notExist string
inputPaths []string
expRefs int
}{
{
desc: "path detected",
input: "test-fixtures",
notExist: "foobar/",
inputPaths: []string{"path-detected/.vimrc"},
expRefs: 1,
},
{
desc: "directory ignored",
input: "test-fixtures",
notExist: "foobar/",
inputPaths: []string{"path-detected"},
expRefs: 0,
},
{
desc: "no files-by-path detected",
input: "test-fixtures",
notExist: "foobar/",
inputPaths: []string{"no-path-detected"},
expRefs: 0,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(test.input)
if err != nil {
t.Errorf("could not create NewDirScope: %+v", err)
}
if src.Metadata.Path != test.input {
t.Errorf("mismatched stringer: '%s' != '%s'", src.Metadata.Path, test.input)
}
_, err = src.FileResolver(SquashedScope)
assert.NoError(t, err)
src.Metadata.Path = test.notExist
resolver2, err := src.FileResolver(SquashedScope)
assert.NoError(t, err)
refs, err := resolver2.FilesByPath(test.inputPaths...)
if err != nil {
t.Errorf("FilesByPath call produced an error: %+v", err)
}
if len(refs) != test.expRefs {
t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expRefs)
}
})
}
}
func TestFilesByPathDoesNotExist(t *testing.T) {
testCases := []struct {
desc string
input string
path string
expected string
}{
{
input: "test-fixtures/path-detected",
desc: "path does not exist",
path: "foo",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(test.input)
if err != nil {
t.Errorf("could not create NewDirScope: %+v", err)
}
resolver, err := src.FileResolver(SquashedScope)
if err != nil {
t.Errorf("could not get resolver error: %+v", err)
}
refs, err := resolver.FilesByPath(test.path)
if err != nil {
t.Errorf("could not get file references from path: %s, %v", test.path, err)
}
if len(refs) != 0 {
t.Errorf("didnt' expect a ref, but got: %d", len(refs))
}
})
}
}
func TestFilesByGlob(t *testing.T) {
testCases := []struct {
desc string
input string
glob string
expected int
}{
{
input: "test-fixtures",
desc: "no matches",
glob: "bar/foo",
expected: 0,
},
{
input: "test-fixtures/path-detected",
desc: "a single match",
glob: "**/*vimrc",
expected: 1,
},
{
input: "test-fixtures/path-detected",
desc: "multiple matches",
glob: "**",
expected: 2,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(test.input)
if err != nil {
t.Errorf("could not create NewDirScope: %+v", err)
}
resolver, err := src.FileResolver(SquashedScope)
if err != nil {
t.Errorf("could not get resolver error: %+v", err)
}
contents, err := resolver.FilesByGlob(test.glob)
if err != nil {
t.Errorf("could not get files by glob: %s+v", err)
}
if len(contents) != test.expected {
t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected)
}
})
}
}
func TestDirectoryExclusions(t *testing.T) {
testCases := []struct {
desc string
input string
glob string
expected int
exclusions []string
err bool
}{
{
input: "test-fixtures/system_paths",
desc: "exclude everything",
glob: "**",
expected: 0,
exclusions: []string{"**/*"},
},
{
input: "test-fixtures/image-simple",
desc: "a single path excluded",
glob: "**",
expected: 3,
exclusions: []string{"**/target/**"},
},
{
input: "test-fixtures/image-simple",
desc: "exclude explicit directory relative to the root",
glob: "**",
expected: 3,
exclusions: []string{"./target"},
},
{
input: "test-fixtures/image-simple",
desc: "exclude explicit file relative to the root",
glob: "**",
expected: 3,
exclusions: []string{"./file-1.txt"},
},
{
input: "test-fixtures/image-simple",
desc: "exclude wildcard relative to the root",
glob: "**",
expected: 2,
exclusions: []string{"./*.txt"},
},
{
input: "test-fixtures/image-simple",
desc: "exclude files deeper",
glob: "**",
expected: 3,
exclusions: []string{"**/really/**"},
},
{
input: "test-fixtures/image-simple",
desc: "files excluded with extension",
glob: "**",
expected: 1,
exclusions: []string{"**/*.txt"},
},
{
input: "test-fixtures/image-simple",
desc: "keep files with different extensions",
glob: "**",
expected: 4,
exclusions: []string{"**/target/**/*.jar"},
},
{
input: "test-fixtures/path-detected",
desc: "file directly excluded",
glob: "**",
expected: 1,
exclusions: []string{"**/empty"},
},
{
input: "test-fixtures/path-detected",
desc: "pattern error containing **/",
glob: "**",
expected: 1,
exclusions: []string{"/**/empty"},
err: true,
},
{
input: "test-fixtures/path-detected",
desc: "pattern error incorrect start",
glob: "**",
expected: 1,
exclusions: []string{"empty"},
err: true,
},
{
input: "test-fixtures/path-detected",
desc: "pattern error starting with /",
glob: "**",
expected: 1,
exclusions: []string{"/empty"},
err: true,
},
}
registryOpts := &image.RegistryOptions{}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
sourceInput, err := ParseInput("dir:"+test.input, "", false)
require.NoError(t, err)
src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
defer fn()
if test.err {
_, err = src.FileResolver(SquashedScope)
if err == nil {
t.Errorf("expected an error for patterns: %s", strings.Join(test.exclusions, " or "))
}
return
}
if err != nil {
t.Errorf("could not create NewDirScope: %+v", err)
}
resolver, err := src.FileResolver(SquashedScope)
if err != nil {
t.Errorf("could not get resolver error: %+v", err)
}
contents, err := resolver.FilesByGlob(test.glob)
if err != nil {
t.Errorf("could not get files by glob: %s+v", err)
}
if len(contents) != test.expected {
t.Errorf("wrong number of files after exclusions (%s): %d != %d", test.glob, len(contents), test.expected)
}
})
}
}
func TestImageExclusions(t *testing.T) {
testCases := []struct {
desc string
input string
glob string
expected int
exclusions []string
}{
// NOTE: in the Dockerfile, /target is moved to /, which makes /really a top-level dir
{
input: "image-simple",
desc: "a single path excluded",
glob: "**",
expected: 2,
exclusions: []string{"/really/**"},
},
{
input: "image-simple",
desc: "a directly referenced directory is excluded",
glob: "**",
expected: 2,
exclusions: []string{"/really"},
},
{
input: "image-simple",
desc: "a partial directory is not excluded",
glob: "**",
expected: 3,
exclusions: []string{"/reall"},
},
{
input: "image-simple",
desc: "exclude files deeper",
glob: "**",
expected: 2,
exclusions: []string{"**/nested/**"},
},
{
input: "image-simple",
desc: "files excluded with extension",
glob: "**",
expected: 2,
exclusions: []string{"**/*1.txt"},
},
{
input: "image-simple",
desc: "keep files with different extensions",
glob: "**",
expected: 3,
exclusions: []string{"**/target/**/*.jar"},
},
{
input: "image-simple",
desc: "file directly excluded",
glob: "**",
expected: 2,
exclusions: []string{"**/somefile-1.txt"}, // file-1 renamed to somefile-1 in Dockerfile
},
}
registryOpts := &image.RegistryOptions{}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input)
sourceInput, err := ParseInput(archiveLocation, "", false)
require.NoError(t, err)
src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
defer fn()
if err != nil {
t.Errorf("could not create NewDirScope: %+v", err)
}
resolver, err := src.FileResolver(SquashedScope)
if err != nil {
t.Errorf("could not get resolver error: %+v", err)
}
contents, err := resolver.FilesByGlob(test.glob)
if err != nil {
t.Errorf("could not get files by glob: %s+v", err)
}
if len(contents) != test.expected {
t.Errorf("wrong number of files after exclusions (%s): %d != %d", test.glob, len(contents), test.expected)
}
})
}
}
func Test_crossPlatformExclusions(t *testing.T) {
testCases := []struct {
desc string
root string
path string
exclude string
match bool
}{
{
desc: "linux doublestar",
root: "/usr",
path: "/usr/var/lib/etc.txt",
exclude: "**/*.txt",
match: true,
},
{
desc: "linux relative",
root: "/usr/var/lib",
path: "/usr/var/lib/etc.txt",
exclude: "./*.txt",
match: true,
},
{
desc: "linux one level",
root: "/usr",
path: "/usr/var/lib/etc.txt",
exclude: "*/*.txt",
match: false,
},
// NOTE: since these tests will run in linux and macOS, the windows paths will be
// considered relative if they do not start with a forward slash and paths with backslashes
// won't be modified by the filepath.ToSlash call, so these are emulating the result of
// filepath.ToSlash usage
{
desc: "windows doublestar",
root: "/C:/User/stuff",
path: "/C:/User/stuff/thing.txt",
exclude: "**/*.txt",
match: true,
},
{
desc: "windows relative",
root: "/C:/User/stuff",
path: "/C:/User/stuff/thing.txt",
exclude: "./*.txt",
match: true,
},
{
desc: "windows one level",
root: "/C:/User/stuff",
path: "/C:/User/stuff/thing.txt",
exclude: "*/*.txt",
match: false,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude})
require.NoError(t, err)
for _, f := range fns {
result := f(test.path, nil)
require.Equal(t, test.match, result)
}
})
}
}
// createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath.
func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string) {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("unable to get cwd: %+v", err)
}
cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
cmd.Dir = filepath.Join(cwd, "test-fixtures")
if err := cmd.Start(); err != nil {
t.Fatalf("unable to start generate zip fixture script: %+v", err)
}
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() != 0 {
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
t.Fatalf("unable to get generate fixture script result: %+v", err)
}
}
}
// setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function,
// which should be called (typically deferred) by the caller, the path of the created tar archive, and an error,
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
// (even if there's an error), and it should always be called.
func setupArchiveTest(t testing.TB, sourceDirPath string) string {
t.Helper()
archivePrefix, err := ioutil.TempFile("", "syft-archive-TEST-")
require.NoError(t, err)
t.Cleanup(
assertNoError(t,
func() error {
return os.Remove(archivePrefix.Name())
},
),
)
destinationArchiveFilePath := archivePrefix.Name() + ".tar"
t.Logf("archive path: %s", destinationArchiveFilePath)
createArchive(t, sourceDirPath, destinationArchiveFilePath)
t.Cleanup(
assertNoError(t,
func() error {
return os.Remove(destinationArchiveFilePath)
},
),
)
cwd, err := os.Getwd()
require.NoError(t, err)
t.Logf("running from: %s", cwd)
return destinationArchiveFilePath
}
func assertNoError(t testing.TB, fn func() error) func() {
return func() {
assert.NoError(t, fn())
}
}