mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
Add --exclude flag (#695)
This commit is contained in:
parent
a27907659d
commit
006ba9b557
9 changed files with 672 additions and 25 deletions
23
README.md
23
README.md
|
@ -93,6 +93,23 @@ file:path/to/yourproject/file read directly from a path on disk (any si
|
|||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
### Excluding file paths
|
||||
|
||||
Syft can exclude files and paths from being scanned within a source by using glob expressions
|
||||
with one or more `--exclude` parameters:
|
||||
```
|
||||
syft <source> --exclude './out/**/*.json' --exclude /etc
|
||||
```
|
||||
**Note:** in the case of _image scanning_, since the entire filesystem is scanned it is
|
||||
possible to use absolute paths like `/etc` or `/usr/**/*.txt` whereas _directory scans_
|
||||
exclude files _relative to the specified directory_. For example: scanning `/usr/foo` with
|
||||
`--exclude ./package.json` would exclude `/usr/foo/package.json` and `--exclude '**/package.json'`
|
||||
would exclude all `package.json` files under `/usr/foo`. For _directory scans_,
|
||||
it is required to begin path expressions with `./`, `*/`, or `**/`, all of which
|
||||
will be resolved _relative to the specified scan directory_. Keep in mind, your shell
|
||||
may attempt to expand wildcards, so put those parameters in single quotes, like:
|
||||
`'**/*.json'`.
|
||||
|
||||
### Output formats
|
||||
|
||||
The output format for Syft is configurable as well:
|
||||
|
@ -219,6 +236,12 @@ file: ""
|
|||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||
# exclude:
|
||||
# - '/etc/**'
|
||||
# - './out/**/*.json'
|
||||
exclude:
|
||||
|
||||
# cataloging packages is exposed through the packages and power-user subcommands
|
||||
package:
|
||||
cataloger:
|
||||
|
|
|
@ -132,6 +132,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
|||
"include dockerfile for upload to Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.StringArrayP(
|
||||
"exclude", "", nil,
|
||||
"exclude paths from being scanned using a glob expression",
|
||||
)
|
||||
|
||||
flags.Bool(
|
||||
"overwrite-existing-image", false,
|
||||
"overwrite an existing image during the upload to Anchore Enterprise",
|
||||
|
@ -158,6 +163,10 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
|
@ -253,7 +262,7 @@ func packagesExecWorker(userInput string) <-chan error {
|
|||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions...)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to determine image source: %w", err)
|
||||
return
|
||||
|
|
|
@ -113,7 +113,7 @@ func powerUserExecWorker(userInput string) <-chan error {
|
|||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions...)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
|
|
@ -44,6 +44,7 @@ type Application struct {
|
|||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
}
|
||||
|
||||
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
@ -59,17 +60,13 @@ func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryR
|
|||
currentWdRelRoot = filepath.Clean(root)
|
||||
}
|
||||
|
||||
if pathFilters == nil {
|
||||
pathFilters = []pathFilterFn{isUnallowableFileType, isUnixSystemRuntimePath}
|
||||
}
|
||||
|
||||
resolver := directoryResolver{
|
||||
path: root,
|
||||
currentWd: currentWd,
|
||||
currentWdRelativeToRoot: currentWdRelRoot,
|
||||
fileTree: filetree.NewFileTree(),
|
||||
metadata: make(map[file.ID]FileMetadata),
|
||||
pathFilterFns: pathFilters,
|
||||
pathFilterFns: append([]pathFilterFn{isUnallowableFileType, isUnixSystemRuntimePath}, pathFilters...),
|
||||
refsByMIMEType: make(map[string][]file.Reference),
|
||||
errPaths: make(map[string]error),
|
||||
}
|
||||
|
@ -95,7 +92,7 @@ func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]st
|
|||
fi, err := os.Stat(root)
|
||||
if err != nil && fi != nil && !fi.IsDir() {
|
||||
// note: we want to index the path regardless of an error stat-ing the path
|
||||
newRoot := r.indexPath(root, fi, nil)
|
||||
newRoot, _ := r.indexPath(root, fi, nil)
|
||||
if newRoot != "" {
|
||||
roots = append(roots, newRoot)
|
||||
}
|
||||
|
@ -106,7 +103,12 @@ func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]st
|
|||
func(path string, info os.FileInfo, err error) error {
|
||||
stager.Current = path
|
||||
|
||||
newRoot := r.indexPath(path, info, err)
|
||||
newRoot, err := r.indexPath(path, info, err)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newRoot != "" {
|
||||
roots = append(roots, newRoot)
|
||||
}
|
||||
|
@ -115,35 +117,38 @@ func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]st
|
|||
})
|
||||
}
|
||||
|
||||
func (r *directoryResolver) indexPath(path string, info os.FileInfo, err error) string {
|
||||
func (r *directoryResolver) indexPath(path string, info os.FileInfo, err error) (string, error) {
|
||||
// ignore any path which a filter function returns true
|
||||
for _, filterFn := range r.pathFilterFns {
|
||||
if filterFn(path, info) {
|
||||
return ""
|
||||
if filterFn != nil && filterFn(path, info) {
|
||||
if info.IsDir() {
|
||||
return "", fs.SkipDir
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.isFileAccessErr(path, err) {
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// link cycles could cause a revisit --we should not allow this
|
||||
if r.fileTree.HasPath(file.Path(path)) {
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
// walk may not be able to provide a FileInfo object, don't allow for this to stop indexing; keep track of the paths and continue.
|
||||
r.errPaths[path] = fmt.Errorf("no file info observable at path=%q", path)
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
newRoot, err := r.addPathToIndex(path, info)
|
||||
if r.isFileAccessErr(path, err) {
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return newRoot
|
||||
return newRoot, nil
|
||||
}
|
||||
|
||||
func (r *directoryResolver) isFileAccessErr(path string, err error) bool {
|
||||
|
|
101
syft/source/excluding_file_resolver.go
Normal file
101
syft/source/excluding_file_resolver.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type excludeFn func(string) bool
|
||||
|
||||
// excludingResolver decorates a resolver with an exclusion function that is used to
|
||||
// filter out entries in the delegate resolver
|
||||
type excludingResolver struct {
|
||||
delegate FileResolver
|
||||
excludeFn excludeFn
|
||||
}
|
||||
|
||||
// NewExcludingResolver create a new resolver which wraps the provided delegate and excludes
|
||||
// entries based on a provided path exclusion function
|
||||
func NewExcludingResolver(delegate FileResolver, excludeFn excludeFn) FileResolver {
|
||||
return &excludingResolver{
|
||||
delegate,
|
||||
excludeFn,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *excludingResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
|
||||
if locationMatches(&location, r.excludeFn) {
|
||||
return nil, fmt.Errorf("no such location: %+v", location.RealPath)
|
||||
}
|
||||
return r.delegate.FileContentsByLocation(location)
|
||||
}
|
||||
|
||||
func (r *excludingResolver) FileMetadataByLocation(location Location) (FileMetadata, error) {
|
||||
if locationMatches(&location, r.excludeFn) {
|
||||
return FileMetadata{}, fmt.Errorf("no such location: %+v", location.RealPath)
|
||||
}
|
||||
return r.delegate.FileMetadataByLocation(location)
|
||||
}
|
||||
|
||||
func (r *excludingResolver) HasPath(path string) bool {
|
||||
if r.excludeFn(path) {
|
||||
return false
|
||||
}
|
||||
return r.delegate.HasPath(path)
|
||||
}
|
||||
|
||||
func (r *excludingResolver) FilesByPath(paths ...string) ([]Location, error) {
|
||||
locations, err := r.delegate.FilesByPath(paths...)
|
||||
return filterLocations(locations, err, r.excludeFn)
|
||||
}
|
||||
|
||||
func (r *excludingResolver) FilesByGlob(patterns ...string) ([]Location, error) {
|
||||
locations, err := r.delegate.FilesByGlob(patterns...)
|
||||
return filterLocations(locations, err, r.excludeFn)
|
||||
}
|
||||
|
||||
func (r *excludingResolver) FilesByMIMEType(types ...string) ([]Location, error) {
|
||||
locations, err := r.delegate.FilesByMIMEType(types...)
|
||||
return filterLocations(locations, err, r.excludeFn)
|
||||
}
|
||||
|
||||
func (r *excludingResolver) RelativeFileByPath(location Location, path string) *Location {
|
||||
l := r.delegate.RelativeFileByPath(location, path)
|
||||
if l != nil && locationMatches(l, r.excludeFn) {
|
||||
return nil
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (r *excludingResolver) AllLocations() <-chan Location {
|
||||
c := make(chan Location)
|
||||
go func() {
|
||||
defer close(c)
|
||||
for location := range r.delegate.AllLocations() {
|
||||
if !locationMatches(&location, r.excludeFn) {
|
||||
c <- location
|
||||
}
|
||||
}
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
func locationMatches(location *Location, exclusionFn excludeFn) bool {
|
||||
return exclusionFn(location.RealPath) || exclusionFn(location.VirtualPath)
|
||||
}
|
||||
|
||||
func filterLocations(locations []Location, err error, exclusionFn excludeFn) ([]Location, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exclusionFn != nil {
|
||||
for i := 0; i < len(locations); i++ {
|
||||
location := &locations[i]
|
||||
if locationMatches(location, exclusionFn) {
|
||||
locations = append(locations[:i], locations[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
return locations, nil
|
||||
}
|
197
syft/source/excluding_file_resolver_test.go
Normal file
197
syft/source/excluding_file_resolver_test.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExcludingResolver(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
locations []string
|
||||
excludeFn excludeFn
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "keeps locations",
|
||||
locations: []string{"a", "b", "c"},
|
||||
excludeFn: func(s string) bool {
|
||||
return false
|
||||
},
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "removes locations",
|
||||
locations: []string{"d", "e", "f"},
|
||||
excludeFn: func(s string) bool {
|
||||
return true
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "removes first match",
|
||||
locations: []string{"g", "h", "i"},
|
||||
excludeFn: func(s string) bool {
|
||||
return s == "g"
|
||||
},
|
||||
expected: []string{"h", "i"},
|
||||
},
|
||||
{
|
||||
name: "removes last match",
|
||||
locations: []string{"j", "k", "l"},
|
||||
excludeFn: func(s string) bool {
|
||||
return s == "l"
|
||||
},
|
||||
expected: []string{"j", "k"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resolver := &mockResolver{
|
||||
locations: test.locations,
|
||||
}
|
||||
excludingResolver := NewExcludingResolver(resolver, test.excludeFn)
|
||||
|
||||
locations, _ := excludingResolver.FilesByPath()
|
||||
assert.ElementsMatch(t, locationPaths(locations), test.expected)
|
||||
|
||||
locations, _ = excludingResolver.FilesByGlob()
|
||||
assert.ElementsMatch(t, locationPaths(locations), test.expected)
|
||||
|
||||
locations, _ = excludingResolver.FilesByMIMEType()
|
||||
assert.ElementsMatch(t, locationPaths(locations), test.expected)
|
||||
|
||||
locations = []Location{}
|
||||
|
||||
channel := excludingResolver.AllLocations()
|
||||
for location := range channel {
|
||||
locations = append(locations, location)
|
||||
}
|
||||
assert.ElementsMatch(t, locationPaths(locations), test.expected)
|
||||
|
||||
diff := difference(test.locations, test.expected)
|
||||
|
||||
for _, path := range diff {
|
||||
assert.False(t, excludingResolver.HasPath(path))
|
||||
c, err := excludingResolver.FileContentsByLocation(makeLocation(path))
|
||||
assert.Nil(t, c)
|
||||
assert.Error(t, err)
|
||||
m, err := excludingResolver.FileMetadataByLocation(makeLocation(path))
|
||||
assert.Empty(t, m.LinkDestination)
|
||||
assert.Error(t, err)
|
||||
l := excludingResolver.RelativeFileByPath(makeLocation(""), path)
|
||||
assert.Nil(t, l)
|
||||
}
|
||||
|
||||
for _, path := range test.expected {
|
||||
assert.True(t, excludingResolver.HasPath(path))
|
||||
c, err := excludingResolver.FileContentsByLocation(makeLocation(path))
|
||||
assert.NotNil(t, c)
|
||||
assert.Nil(t, err)
|
||||
m, err := excludingResolver.FileMetadataByLocation(makeLocation(path))
|
||||
assert.NotEmpty(t, m.LinkDestination)
|
||||
assert.Nil(t, err)
|
||||
l := excludingResolver.RelativeFileByPath(makeLocation(""), path)
|
||||
assert.NotNil(t, l)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// difference returns the elements in `a` that aren't in `b`.
|
||||
func difference(a, b []string) []string {
|
||||
mb := make(map[string]struct{}, len(b))
|
||||
for _, x := range b {
|
||||
mb[x] = struct{}{}
|
||||
}
|
||||
var diff []string
|
||||
for _, x := range a {
|
||||
if _, found := mb[x]; !found {
|
||||
diff = append(diff, x)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
func makeLocation(path string) Location {
|
||||
return Location{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: path,
|
||||
FileSystemID: "",
|
||||
},
|
||||
VirtualPath: "",
|
||||
ref: file.Reference{},
|
||||
}
|
||||
}
|
||||
|
||||
func locationPaths(locations []Location) []string {
|
||||
paths := []string{}
|
||||
for _, l := range locations {
|
||||
paths = append(paths, l.RealPath)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
type mockResolver struct {
|
||||
locations []string
|
||||
}
|
||||
|
||||
func (r *mockResolver) getLocations() ([]Location, error) {
|
||||
out := []Location{}
|
||||
for _, path := range r.locations {
|
||||
out = append(out, makeLocation(path))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *mockResolver) FileContentsByLocation(_ Location) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("Hello, world!")), nil
|
||||
}
|
||||
|
||||
func (r *mockResolver) FileMetadataByLocation(_ Location) (FileMetadata, error) {
|
||||
return FileMetadata{
|
||||
LinkDestination: "MOCK",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mockResolver) HasPath(_ string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *mockResolver) FilesByPath(_ ...string) ([]Location, error) {
|
||||
return r.getLocations()
|
||||
}
|
||||
|
||||
func (r *mockResolver) FilesByGlob(_ ...string) ([]Location, error) {
|
||||
return r.getLocations()
|
||||
}
|
||||
|
||||
func (r *mockResolver) FilesByMIMEType(_ ...string) ([]Location, error) {
|
||||
return r.getLocations()
|
||||
}
|
||||
|
||||
func (r *mockResolver) RelativeFileByPath(_ Location, path string) *Location {
|
||||
return &Location{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: path,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mockResolver) AllLocations() <-chan Location {
|
||||
c := make(chan Location)
|
||||
go func() {
|
||||
defer close(c)
|
||||
locations, _ := r.getLocations()
|
||||
for _, location := range locations {
|
||||
c <- location
|
||||
}
|
||||
}()
|
||||
return c
|
||||
}
|
|
@ -9,11 +9,14 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/bmatcuk/doublestar/v2"
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
@ -26,28 +29,38 @@ type Source struct {
|
|||
directoryResolver *directoryResolver
|
||||
path string
|
||||
mutex *sync.Mutex
|
||||
Exclusions []string
|
||||
}
|
||||
|
||||
type sourceDetector func(string) (image.Source, string, error)
|
||||
|
||||
// New produces a Source based on userInput like dir: or image:tag
|
||||
func New(userInput string, registryOptions *image.RegistryOptions) (*Source, func(), error) {
|
||||
func New(userInput string, registryOptions *image.RegistryOptions, exclusions ...string) (*Source, func(), error) {
|
||||
fs := afero.NewOsFs()
|
||||
parsedScheme, imageSource, location, err := detectScheme(fs, image.DetectSource, userInput)
|
||||
if err != nil {
|
||||
return &Source{}, func() {}, fmt.Errorf("unable to parse input=%q: %w", userInput, err)
|
||||
}
|
||||
|
||||
source := &Source{}
|
||||
cleanupFn := func() {}
|
||||
|
||||
switch parsedScheme {
|
||||
case FileScheme:
|
||||
return generateFileSource(fs, location)
|
||||
source, cleanupFn, err = generateFileSource(fs, location)
|
||||
case DirectoryScheme:
|
||||
return generateDirectorySource(fs, location)
|
||||
source, cleanupFn, err = generateDirectorySource(fs, location)
|
||||
case ImageScheme:
|
||||
return generateImageSource(location, userInput, imageSource, registryOptions)
|
||||
source, cleanupFn, err = generateImageSource(location, userInput, imageSource, registryOptions)
|
||||
default:
|
||||
err = fmt.Errorf("unable to process input for scanning: '%s'", userInput)
|
||||
}
|
||||
|
||||
return &Source{}, func() {}, fmt.Errorf("unable to process input for scanning: '%s'", userInput)
|
||||
if err == nil {
|
||||
source.Exclusions = exclusions
|
||||
}
|
||||
|
||||
return source, cleanupFn, err
|
||||
}
|
||||
|
||||
func generateImageSource(location, userInput string, imageSource image.Source, registryOptions *image.RegistryOptions) (*Source, func(), error) {
|
||||
|
@ -180,7 +193,11 @@ func (s *Source) FileResolver(scope Scope) (FileResolver, error) {
|
|||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if s.directoryResolver == nil {
|
||||
resolver, err := newDirectoryResolver(s.path)
|
||||
exclusionFunctions, err := getDirectoryExclusionFunctions(s.path, s.Exclusions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolver, err := newDirectoryResolver(s.path, exclusionFunctions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -188,14 +205,24 @@ func (s *Source) FileResolver(scope Scope) (FileResolver, error) {
|
|||
}
|
||||
return s.directoryResolver, nil
|
||||
case ImageScheme:
|
||||
var resolver FileResolver
|
||||
var err error
|
||||
switch scope {
|
||||
case SquashedScope:
|
||||
return newImageSquashResolver(s.Image)
|
||||
resolver, err = newImageSquashResolver(s.Image)
|
||||
case AllLayersScope:
|
||||
return newAllLayersResolver(s.Image)
|
||||
resolver, err = newAllLayersResolver(s.Image)
|
||||
default:
|
||||
return nil, fmt.Errorf("bad image scope provided: %+v", scope)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// image tree contains all paths, so we filter out the excluded entries afterwards
|
||||
if len(s.Exclusions) > 0 {
|
||||
resolver = NewExcludingResolver(resolver, getImageExclusionFunction(s.Exclusions))
|
||||
}
|
||||
return resolver, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unable to determine FilePathResolver with current scheme=%q", s.Metadata.Scheme)
|
||||
}
|
||||
|
@ -214,3 +241,71 @@ func unarchiveToTmp(path string, unarchiver archiver.Unarchiver) (string, func()
|
|||
|
||||
return tempDir, cleanupFn, unarchiver.Unarchive(path, tempDir)
|
||||
}
|
||||
|
||||
func getImageExclusionFunction(exclusions []string) func(string) bool {
|
||||
if len(exclusions) == 0 {
|
||||
return nil
|
||||
}
|
||||
// add subpath exclusions
|
||||
for _, exclusion := range exclusions {
|
||||
exclusions = append(exclusions, exclusion+"/**")
|
||||
}
|
||||
return func(path string) bool {
|
||||
for _, exclusion := range exclusions {
|
||||
matches, err := doublestar.Match(exclusion, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func getDirectoryExclusionFunctions(root string, exclusions []string) ([]pathFilterFn, error) {
|
||||
if len(exclusions) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// this is what directoryResolver.indexTree is doing to get the absolute path:
|
||||
root, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
|
||||
var errors []string
|
||||
for idx, exclusion := range exclusions {
|
||||
// check exclusions for supported paths, these are all relative to the "scan root"
|
||||
if strings.HasPrefix(exclusion, "./") || strings.HasPrefix(exclusion, "*/") || strings.HasPrefix(exclusion, "**/") {
|
||||
exclusion = strings.TrimPrefix(exclusion, "./")
|
||||
exclusions[idx] = root + exclusion
|
||||
} else {
|
||||
errors = append(errors, exclusion)
|
||||
}
|
||||
}
|
||||
|
||||
if errors != nil {
|
||||
return nil, fmt.Errorf("invalid exclusion pattern(s): '%s' (must start with one of: './', '*/', or '**/')", strings.Join(errors, "', '"))
|
||||
}
|
||||
|
||||
return []pathFilterFn{
|
||||
func(path string, _ os.FileInfo) bool {
|
||||
for _, exclusion := range exclusions {
|
||||
matches, err := doublestar.Match(exclusion, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -6,9 +6,12 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -317,6 +320,219 @@ func TestFilesByGlob(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
src, fn, err := New("dir:"+test.input, 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)
|
||||
src, fn, err := New(archiveLocation, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
|
Loading…
Reference in a new issue