mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
* test: add windows unit test job and capture dir resolver path bug - add a `Unit tests (Windows)` job running a new `make unit:windows` task, scoped to the packages that own host path handling (tests needing linux docker fixtures are skipped) - `Unit tests` is now a gate job over the Linux and Windows unit jobs - add a resolver test asserting that locations from `AllLocations` resolve again via `FilesByPath` and use posix paths. This is expected to fail on windows today, which is why file digests come back empty for `dir:` scans (#5325) - fix windows-only test files that no longer compiled Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: drop verbose output from integration tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: windows path handling outside of the file lookup bug - chroot paths are always posix (`ToChrootPath` no longer returns backslash paths on windows), same for the directory source name derived from `base` - `windows.FromPosix` no longer panics on input that isn't volume-encoded (e.g. relative or already native paths) - skip tests on windows that rely on symlink fixtures, shell script fixtures, or emulate windows paths on a posix host - disable `core.autocrlf` on the windows CI job so fixture digests match linux This leaves only the `FilesByPath` lookup failures from #5325. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test: skip posix-only fileresolver tests on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: split contained paths on posix separators on windows - `allContainedPaths` cleaned with the native separator but split on `/`, so a root reached through a symlink never had its ancestors indexed on windows - index assertions in tests now key on the posix path the index actually stores - skip symlink fixture tests and a posix-only test helper on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: revert unrelated go.sum churn and allContainedPaths change - restore `go.sum` and `.make/go.sum` (accidentally populated by a local `-mod=mod` invocation) - revert the `allContainedPaths` separator change: production passes native windows paths there, and splitting them on `/` produces drive-relative ancestors, so skip the posix-only test on windows instead - annotate the remaining `filepath` usages that operate on native paths Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: search the file tree with posix paths on windows (#5325) The directory and file resolvers key their file tree on posix paths (`/c/some/path`), but `FilesByPath`, `HasPath` and root-anchored `FilesByGlob` searched it with the native path from the chroot context (`C:\some\path`). On windows every lookup silently missed, so anything that resolves files by path found nothing. This is why file digests were empty for `dir:` scans. Requests are now converted to posix before searching the tree. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
229 lines
6.4 KiB
Go
229 lines
6.4 KiB
Go
package directorysource
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/bmatcuk/doublestar/v4"
|
|
"github.com/opencontainers/go-digest"
|
|
|
|
"github.com/anchore/syft/internal/log"
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
|
"github.com/anchore/syft/syft/source"
|
|
"github.com/anchore/syft/syft/source/internal"
|
|
)
|
|
|
|
var _ source.Source = (*directorySource)(nil)
|
|
|
|
type Config struct {
|
|
Path string
|
|
Base string
|
|
Exclude source.ExcludeConfig
|
|
Alias source.Alias
|
|
}
|
|
|
|
type directorySource struct {
|
|
id artifact.ID
|
|
config Config
|
|
resolver file.Resolver
|
|
mutex *sync.Mutex
|
|
}
|
|
|
|
func NewFromPath(path string) (source.Source, error) {
|
|
return New(Config{Path: path})
|
|
}
|
|
|
|
func New(cfg Config) (source.Source, error) {
|
|
fileMeta, err := os.Stat(cfg.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
|
|
}
|
|
|
|
if !fileMeta.IsDir() {
|
|
return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
|
|
}
|
|
|
|
return &directorySource{
|
|
id: deriveIDFromDirectory(cfg),
|
|
config: cfg,
|
|
mutex: &sync.Mutex{},
|
|
}, nil
|
|
}
|
|
|
|
func (s directorySource) ID() artifact.ID {
|
|
return s.id
|
|
}
|
|
|
|
func (s directorySource) Describe() source.Description {
|
|
name := cleanDirPath(s.config.Path, s.config.Base)
|
|
version := ""
|
|
supplier := ""
|
|
if !s.config.Alias.IsEmpty() {
|
|
a := s.config.Alias
|
|
if a.Name != "" {
|
|
name = a.Name
|
|
}
|
|
|
|
if a.Version != "" {
|
|
version = a.Version
|
|
}
|
|
|
|
if a.Supplier != "" {
|
|
supplier = a.Supplier
|
|
}
|
|
}
|
|
return source.Description{
|
|
ID: string(s.id),
|
|
Name: name,
|
|
Version: version,
|
|
Supplier: supplier,
|
|
Metadata: source.DirectoryMetadata{
|
|
Path: s.config.Path,
|
|
Base: s.config.Base,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
if s.resolver != nil {
|
|
return s.resolver, nil
|
|
}
|
|
|
|
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// this should be the only file resolver that might have overlap with where files are cached
|
|
exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...)
|
|
|
|
res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
|
|
}
|
|
|
|
s.resolver = res
|
|
return s.resolver, nil
|
|
}
|
|
|
|
func (s *directorySource) Close() error {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
s.resolver = nil
|
|
return nil
|
|
}
|
|
|
|
func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, 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
|
|
}
|
|
|
|
// this handles Windows file paths by converting them to C:/something/else format
|
|
root = filepath.ToSlash(root)
|
|
|
|
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, "./")
|
|
// a trailing slash signals a directory but is otherwise discarded by doublestar.Match,
|
|
// causing the pattern to silently match nothing (see issue #4839)
|
|
exclusion = strings.TrimSuffix(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 []fileresolver.PathIndexVisitor{
|
|
func(_, path string, info os.FileInfo, _ error) error {
|
|
for _, exclusion := range exclusions {
|
|
// this is required to handle Windows filepaths
|
|
path = filepath.ToSlash(path)
|
|
matches, err := doublestar.Match(exclusion, path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if matches {
|
|
if info != nil && info.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return fileresolver.ErrSkipPath
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then
|
|
// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived
|
|
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
|
|
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
|
|
// consideration for the path.
|
|
func deriveIDFromDirectory(cfg Config) artifact.ID {
|
|
var info string
|
|
if !cfg.Alias.IsEmpty() {
|
|
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
|
|
// why? this allows the user to set a dependable stable value for the artifact ID in case the
|
|
// scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again).
|
|
info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version)
|
|
} else {
|
|
log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)")
|
|
info = cleanDirPath(cfg.Path, cfg.Base)
|
|
}
|
|
|
|
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
|
|
}
|
|
|
|
func cleanDirPath(path, base string) string {
|
|
if path == base {
|
|
return path
|
|
}
|
|
|
|
if base != "" {
|
|
cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path)
|
|
cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base)
|
|
|
|
if rootErr == nil && baseErr == nil {
|
|
// allows for normalizing inputs:
|
|
// cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path
|
|
// cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001
|
|
// normalized: some/path
|
|
|
|
relPath, err := filepath.Rel(cleanBase, cleanRoot)
|
|
if err == nil {
|
|
// filepath.Rel yields a native path, but source names are posix like every other path syft reports
|
|
path = filepath.ToSlash(relPath)
|
|
}
|
|
// this is odd, but this means we can't use base
|
|
}
|
|
// if the base is not a valid chroot, then just use the path as-is
|
|
}
|
|
|
|
return path
|
|
}
|