feat: add --from flag, refactor source providers (#2610)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Keith Zantow
2024-02-27 16:44:37 -05:00
committed by GitHub
parent 928511ea0f
commit a978966cad
72 changed files with 1035 additions and 1080 deletions
-207
View File
@@ -1,207 +0,0 @@
package source
import (
"crypto"
"fmt"
"strings"
"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/anchore/stereoscope/pkg/image"
)
type detectedType string
const (
// unknownType is the default scheme
unknownType detectedType = "unknown-type"
// directoryType indicates the source being cataloged is a directory on the root filesystem
directoryType detectedType = "directory-type"
// containerImageType indicates the source being cataloged is a container image
containerImageType detectedType = "container-image-type"
// fileType indicates the source being cataloged is a single file
fileType detectedType = "file-type"
)
type sourceResolver func(string) (image.Source, string, error)
// Detection is an object that captures the detected user input regarding source location, scheme, and provider type.
// It acts as a struct input for some source constructors.
type Detection struct {
detectedType detectedType
imageSource image.Source
location string
}
func (d Detection) IsContainerImage() bool {
return d.detectedType == containerImageType
}
type DetectConfig struct {
DefaultImageSource string
}
func DefaultDetectConfig() DetectConfig {
return DetectConfig{}
}
// Detect generates a source Detection that can be used as an argument to generate a new source
// from specific providers including a registry, with an explicit name.
func Detect(userInput string, cfg DetectConfig) (*Detection, error) {
fs := afero.NewOsFs()
ty, src, location, err := detect(fs, image.DetectSource, userInput)
if err != nil {
return nil, err
}
if src == image.UnknownSource {
// only run for these two schemes
// only check on scan command, attest we automatically try to pull from userInput
switch ty {
case containerImageType, unknownType:
ty = containerImageType
location = userInput
if cfg.DefaultImageSource != "" {
src = parseDefaultImageSource(cfg.DefaultImageSource)
} else {
src = image.DetermineDefaultImagePullSource(userInput)
}
}
}
// collect user input for downstream consumption
return &Detection{
detectedType: ty,
imageSource: src,
location: location,
}, nil
}
type DetectionSourceConfig struct {
Alias Alias
RegistryOptions *image.RegistryOptions
Platform *image.Platform
Exclude ExcludeConfig
DigestAlgorithms []crypto.Hash
BasePath string
}
func DefaultDetectionSourceConfig() DetectionSourceConfig {
return DetectionSourceConfig{
DigestAlgorithms: []crypto.Hash{
crypto.SHA256,
},
}
}
// NewSource produces a Source based on userInput like dir: or image:tag
func (d Detection) NewSource(cfg DetectionSourceConfig) (Source, error) {
var err error
var src Source
if d.detectedType != containerImageType && cfg.Platform != nil {
return nil, fmt.Errorf("cannot specify a platform for a non-image source")
}
switch d.detectedType {
case fileType:
src, err = NewFromFile(
FileConfig{
Path: d.location,
Exclude: cfg.Exclude,
DigestAlgorithms: cfg.DigestAlgorithms,
Alias: cfg.Alias,
},
)
case directoryType:
base := cfg.BasePath
if base == "" {
base = d.location
}
src, err = NewFromDirectory(
DirectoryConfig{
Path: d.location,
Base: base,
Exclude: cfg.Exclude,
Alias: cfg.Alias,
},
)
case containerImageType:
src, err = NewFromStereoscopeImage(
StereoscopeImageConfig{
Reference: d.location,
From: d.imageSource,
Platform: cfg.Platform,
RegistryOptions: cfg.RegistryOptions,
Exclude: cfg.Exclude,
Alias: cfg.Alias,
},
)
default:
err = fmt.Errorf("unable to process input for scanning")
}
return src, err
}
func detect(fs afero.Fs, imageSourceResolver sourceResolver, userInput string) (detectedType, image.Source, string, error) {
switch {
case strings.HasPrefix(userInput, "dir:"):
dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:"))
if err != nil {
return unknownType, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
}
return directoryType, image.UnknownSource, dirLocation, nil
case strings.HasPrefix(userInput, "file:"):
fileLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "file:"))
if err != nil {
return unknownType, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
}
return fileType, image.UnknownSource, fileLocation, nil
}
// try the most specific sources first and move out towards more generic sources.
// first: let's try the image detector, which has more scheme parsing internal to stereoscope
src, imageSpec, err := imageSourceResolver(userInput)
if err == nil && src != image.UnknownSource {
return containerImageType, src, imageSpec, nil
}
// next: let's try more generic sources (dir, file, etc.)
location, err := homedir.Expand(userInput)
if err != nil {
return unknownType, image.UnknownSource, "", fmt.Errorf("unable to expand potential directory path: %w", err)
}
fileMeta, err := fs.Stat(location)
if err != nil {
return unknownType, src, "", nil
}
if fileMeta.IsDir() {
return directoryType, src, location, nil
}
return fileType, src, location, nil
}
func parseDefaultImageSource(defaultImageSource string) image.Source {
switch defaultImageSource {
case "registry":
return image.OciRegistrySource
case "docker":
return image.DockerDaemonSource
case "podman":
return image.PodmanDaemonSource
case "containerd":
return image.ContainerdDaemonSource
default:
return image.UnknownSource
}
}
-328
View File
@@ -1,328 +0,0 @@
package source
import (
"os"
"testing"
"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/image"
)
func Test_Detect(t *testing.T) {
type detectorResult struct {
src image.Source
ref string
err error
}
testCases := []struct {
name string
userInput string
dirs []string
files []string
detection detectorResult
expectedScheme detectedType
expectedLocation string
}{
{
name: "docker-image-ref",
userInput: "wagoodman/dive:latest",
detection: detectorResult{
src: image.DockerDaemonSource,
ref: "wagoodman/dive:latest",
},
expectedScheme: containerImageType,
expectedLocation: "wagoodman/dive:latest",
},
{
name: "docker-image-ref-no-tag",
userInput: "wagoodman/dive",
detection: detectorResult{
src: image.DockerDaemonSource,
ref: "wagoodman/dive",
},
expectedScheme: containerImageType,
expectedLocation: "wagoodman/dive",
},
{
name: "registry-image-explicit-scheme",
userInput: "registry:wagoodman/dive:latest",
detection: detectorResult{
src: image.OciRegistrySource,
ref: "wagoodman/dive:latest",
},
expectedScheme: containerImageType,
expectedLocation: "wagoodman/dive:latest",
},
{
name: "docker-image-explicit-scheme",
userInput: "docker:wagoodman/dive:latest",
detection: detectorResult{
src: image.DockerDaemonSource,
ref: "wagoodman/dive:latest",
},
expectedScheme: containerImageType,
expectedLocation: "wagoodman/dive:latest",
},
{
name: "docker-image-explicit-scheme-no-tag",
userInput: "docker:wagoodman/dive",
detection: detectorResult{
src: image.DockerDaemonSource,
ref: "wagoodman/dive",
},
expectedScheme: containerImageType,
expectedLocation: "wagoodman/dive",
},
{
name: "docker-image-edge-case",
userInput: "docker:latest",
detection: detectorResult{
src: image.DockerDaemonSource,
ref: "latest",
},
expectedScheme: containerImageType,
// we expected to be able to handle this case better, however, I don't see a way to do this
// the user will need to provide more explicit input (docker:docker:latest)
expectedLocation: "latest",
},
{
name: "docker-image-edge-case-explicit",
userInput: "docker:docker:latest",
detection: detectorResult{
src: image.DockerDaemonSource,
ref: "docker:latest",
},
expectedScheme: containerImageType,
// we expected to be able to handle this case better, however, I don't see a way to do this
// the user will need to provide more explicit input (docker:docker:latest)
expectedLocation: "docker:latest",
},
{
name: "oci-tar",
userInput: "some/path-to-file",
detection: detectorResult{
src: image.OciTarballSource,
ref: "some/path-to-file",
},
expectedScheme: containerImageType,
expectedLocation: "some/path-to-file",
},
{
name: "oci-dir",
userInput: "some/path-to-dir",
detection: detectorResult{
src: image.OciDirectorySource,
ref: "some/path-to-dir",
},
dirs: []string{"some/path-to-dir"},
expectedScheme: containerImageType,
expectedLocation: "some/path-to-dir",
},
{
name: "guess-dir",
userInput: "some/path-to-dir",
detection: detectorResult{
src: image.UnknownSource,
ref: "",
},
dirs: []string{"some/path-to-dir"},
expectedScheme: directoryType,
expectedLocation: "some/path-to-dir",
},
{
name: "generic-dir-does-not-exist",
userInput: "some/path-to-dir",
detection: detectorResult{
src: image.DockerDaemonSource,
ref: "some/path-to-dir",
},
expectedScheme: containerImageType,
expectedLocation: "some/path-to-dir",
},
{
name: "found-podman-image-scheme",
userInput: "podman:something:latest",
detection: detectorResult{
src: image.PodmanDaemonSource,
ref: "something:latest",
},
expectedScheme: containerImageType,
expectedLocation: "something:latest",
},
{
name: "explicit-dir",
userInput: "dir:some/path-to-dir",
detection: detectorResult{
src: image.UnknownSource,
ref: "",
},
dirs: []string{"some/path-to-dir"},
expectedScheme: directoryType,
expectedLocation: "some/path-to-dir",
},
{
name: "explicit-file",
userInput: "file:some/path-to-file",
detection: detectorResult{
src: image.UnknownSource,
ref: "",
},
files: []string{"some/path-to-file"},
expectedScheme: fileType,
expectedLocation: "some/path-to-file",
},
{
name: "implicit-file",
userInput: "some/path-to-file",
detection: detectorResult{
src: image.UnknownSource,
ref: "",
},
files: []string{"some/path-to-file"},
expectedScheme: fileType,
expectedLocation: "some/path-to-file",
},
{
name: "explicit-current-dir",
userInput: "dir:.",
detection: detectorResult{
src: image.UnknownSource,
ref: "",
},
expectedScheme: directoryType,
expectedLocation: ".",
},
{
name: "current-dir",
userInput: ".",
detection: detectorResult{
src: image.UnknownSource,
ref: "",
},
expectedScheme: directoryType,
expectedLocation: ".",
},
// we should support tilde expansion
{
name: "tilde-expansion-image-implicit",
userInput: "~/some-path",
detection: detectorResult{
src: image.OciDirectorySource,
ref: "~/some-path",
},
expectedScheme: containerImageType,
expectedLocation: "~/some-path",
},
{
name: "tilde-expansion-dir-implicit",
userInput: "~/some-path",
detection: detectorResult{
src: image.UnknownSource,
ref: "",
},
dirs: []string{"~/some-path"},
expectedScheme: directoryType,
expectedLocation: "~/some-path",
},
{
name: "tilde-expansion-dir-explicit-exists",
userInput: "dir:~/some-path",
dirs: []string{"~/some-path"},
expectedScheme: directoryType,
expectedLocation: "~/some-path",
},
{
name: "tilde-expansion-dir-explicit-dne",
userInput: "dir:~/some-path",
expectedScheme: directoryType,
expectedLocation: "~/some-path",
},
{
name: "tilde-expansion-dir-implicit-dne",
userInput: "~/some-path",
expectedScheme: unknownType,
expectedLocation: "",
},
{
name: "podman-image",
userInput: "containerd:anchore/syft",
detection: detectorResult{
src: image.PodmanDaemonSource,
ref: "anchore/syft",
},
expectedScheme: containerImageType,
expectedLocation: "anchore/syft",
},
{
name: "containerd-image",
userInput: "containerd:anchore/syft",
detection: detectorResult{
src: image.ContainerdDaemonSource,
ref: "anchore/syft",
},
expectedScheme: containerImageType,
expectedLocation: "anchore/syft",
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
fs := afero.NewMemMapFs()
for _, p := range test.dirs {
expandedExpectedLocation, err := homedir.Expand(p)
if err != nil {
t.Fatalf("unable to expand path=%q: %+v", p, err)
}
err = fs.Mkdir(expandedExpectedLocation, os.ModePerm)
if err != nil {
t.Fatalf("failed to create dummy dir: %+v", err)
}
}
for _, p := range test.files {
expandedExpectedLocation, err := homedir.Expand(p)
if err != nil {
t.Fatalf("unable to expand path=%q: %+v", p, err)
}
_, err = fs.Create(expandedExpectedLocation)
if err != nil {
t.Fatalf("failed to create dummy file: %+v", err)
}
}
imageDetector := func(string) (image.Source, string, error) {
// lean on the users real home directory value
switch test.detection.src {
case image.OciDirectorySource, image.DockerTarballSource, image.OciTarballSource:
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
if err != nil {
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
}
return test.detection.src, expandedExpectedLocation, test.detection.err
default:
return test.detection.src, test.detection.ref, test.detection.err
}
}
actualScheme, actualSource, actualLocation, err := detect(fs, imageDetector, test.userInput)
if err != nil {
t.Fatalf("unexpected err : %+v", err)
}
assert.Equal(t, test.detection.src, actualSource, "mismatched source")
assert.Equal(t, test.expectedScheme, actualScheme, "mismatched scheme")
// lean on the users real home directory value
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
if err != nil {
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
}
assert.Equal(t, expandedExpectedLocation, actualLocation, "mismatched location")
})
}
}
+6
View File
@@ -0,0 +1,6 @@
package source
type DirectoryMetadata struct {
Path string `json:"path" yaml:"path"`
Base string `json:"-" yaml:"-"` // though this is important, for display purposes it leaks too much information (abs paths)
}
@@ -1,4 +1,4 @@
package source
package directorysource
import (
"fmt"
@@ -14,37 +14,34 @@ import (
"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 = (*DirectorySource)(nil)
var _ source.Source = (*directorySource)(nil)
type DirectoryConfig struct {
type Config struct {
Path string
Base string
Exclude ExcludeConfig
Alias Alias
Exclude source.ExcludeConfig
Alias source.Alias
}
type DirectorySourceMetadata struct {
Path string `json:"path" yaml:"path"`
Base string `json:"-" yaml:"-"` // though this is important, for display purposes it leaks too much information (abs paths)
}
type DirectorySource struct {
type directorySource struct {
id artifact.ID
config DirectoryConfig
config Config
resolver *fileresolver.Directory
mutex *sync.Mutex
}
func NewFromDirectoryPath(path string) (*DirectorySource, error) {
cfg := DirectoryConfig{
func NewFromPath(path string) (source.Source, error) {
cfg := Config{
Path: path,
}
return NewFromDirectory(cfg)
return New(cfg)
}
func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
func New(cfg Config) (source.Source, error) {
fi, err := os.Stat(cfg.Path)
if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
@@ -54,7 +51,7 @@ func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
}
return &DirectorySource{
return &directorySource{
id: deriveIDFromDirectory(cfg),
config: cfg,
mutex: &sync.Mutex{},
@@ -66,7 +63,7 @@ func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
// 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 DirectoryConfig) artifact.ID {
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.
@@ -78,7 +75,7 @@ func deriveIDFromDirectory(cfg DirectoryConfig) artifact.ID {
info = cleanDirPath(cfg.Path, cfg.Base)
}
return artifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
}
func cleanDirPath(path, base string) string {
@@ -108,11 +105,11 @@ func cleanDirPath(path, base string) string {
return path
}
func (s DirectorySource) ID() artifact.ID {
func (s directorySource) ID() artifact.ID {
return s.id
}
func (s DirectorySource) Describe() Description {
func (s directorySource) Describe() source.Description {
name := cleanDirPath(s.config.Path, s.config.Base)
version := ""
if !s.config.Alias.IsEmpty() {
@@ -124,23 +121,23 @@ func (s DirectorySource) Describe() Description {
version = a.Version
}
}
return Description{
return source.Description{
ID: string(s.id),
Name: name,
Version: version,
Metadata: DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: s.config.Path,
Base: s.config.Base,
},
}
}
func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) {
func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.resolver == nil {
exclusionFunctions, err := getDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
if err != nil {
return nil, err
}
@@ -156,14 +153,14 @@ func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) {
return s.resolver, nil
}
func (s *DirectorySource) Close() error {
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) {
func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, error) {
if len(exclusions) == 0 {
return nil, nil
}
@@ -0,0 +1,65 @@
package directorysource
import (
"context"
"fmt"
"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/anchore/syft/syft/source"
)
func NewSourceProvider(path string, exclude source.ExcludeConfig, alias source.Alias, basePath string) source.Provider {
return &directorySourceProvider{
path: path,
basePath: basePath,
exclude: exclude,
alias: alias,
}
}
type directorySourceProvider struct {
path string
basePath string
exclude source.ExcludeConfig
alias source.Alias
}
func (l directorySourceProvider) Name() string {
return "local-directory"
}
func (l directorySourceProvider) Provide(_ context.Context) (source.Source, error) {
location, err := homedir.Expand(l.path)
if err != nil {
return nil, fmt.Errorf("unable to expand potential directory path: %w", err)
}
fs := afero.NewOsFs()
fileMeta, err := fs.Stat(location)
if err != nil {
return nil, fmt.Errorf("unable to stat location: %w", err)
}
if !fileMeta.IsDir() {
return nil, fmt.Errorf("not a directory source: %s", l.path)
}
return New(
Config{
Path: location,
Base: basePath(l.basePath, location),
Exclude: l.exclude,
Alias: l.alias,
},
)
}
// FIXME why is the base always being set instead of left as empty string?
func basePath(base, location string) string {
if base == "" {
base = location
}
return base
}
@@ -1,4 +1,4 @@
package source
package directorysource
import (
"io/fs"
@@ -13,9 +13,13 @@ import (
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/internal/testutil"
"github.com/anchore/syft/syft/source"
)
func TestNewFromDirectory(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct {
desc string
input string
@@ -54,7 +58,7 @@ func TestNewFromDirectory(t *testing.T) {
if test.cxErr == nil {
test.cxErr = require.NoError
}
src, err := NewFromDirectory(DirectoryConfig{
src, err := New(Config{
Path: test.input,
})
test.cxErr(t, err)
@@ -65,9 +69,9 @@ func TestNewFromDirectory(t *testing.T) {
t.Cleanup(func() {
require.NoError(t, src.Close())
})
assert.Equal(t, test.input, src.Describe().Metadata.(DirectorySourceMetadata).Path)
assert.Equal(t, test.input, src.Describe().Metadata.(source.DirectoryMetadata).Path)
res, err := src.FileResolver(SquashedScope)
res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
refs, err := res.FilesByPath(test.inputPaths...)
@@ -82,6 +86,8 @@ func TestNewFromDirectory(t *testing.T) {
}
func Test_DirectorySource_FilesByGlob(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct {
desc string
input string
@@ -109,10 +115,10 @@ func Test_DirectorySource_FilesByGlob(t *testing.T) {
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(DirectoryConfig{Path: test.input})
src, err := New(Config{Path: test.input})
require.NoError(t, err)
res, err := src.FileResolver(SquashedScope)
res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
@@ -129,6 +135,8 @@ func Test_DirectorySource_FilesByGlob(t *testing.T) {
}
func Test_DirectorySource_Exclusions(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct {
desc string
input string
@@ -270,9 +278,9 @@ func Test_DirectorySource_Exclusions(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(DirectoryConfig{
src, err := New(Config{
Path: test.input,
Exclude: ExcludeConfig{
Exclude: source.ExcludeConfig{
Paths: test.exclusions,
},
})
@@ -282,13 +290,13 @@ func Test_DirectorySource_Exclusions(t *testing.T) {
})
if test.err {
_, err = src.FileResolver(SquashedScope)
_, err = src.FileResolver(source.SquashedScope)
require.Error(t, err)
return
}
require.NoError(t, err)
res, err := src.FileResolver(SquashedScope)
res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
locations, err := res.FilesByGlob(test.glob)
@@ -388,7 +396,7 @@ func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude})
fns, err := GetDirectoryExclusionFunctions(test.root, []string{test.exclude})
require.NoError(t, err)
for _, f := range fns {
@@ -400,6 +408,8 @@ func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
}
func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct {
desc string
input string
@@ -414,13 +424,13 @@ func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(DirectoryConfig{Path: test.input})
src, err := New(Config{Path: test.input})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
res, err := src.FileResolver(SquashedScope)
res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
refs, err := res.FilesByPath(test.path)
@@ -432,41 +442,43 @@ func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
}
func Test_DirectorySource_ID(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
tests := []struct {
name string
cfg DirectoryConfig
cfg Config
want artifact.ID
wantErr require.ErrorAssertionFunc
}{
{
name: "empty",
cfg: DirectoryConfig{},
cfg: Config{},
wantErr: require.Error,
},
{
name: "to a non-existent directory",
cfg: DirectoryConfig{
cfg: Config{
Path: "./test-fixtures/does-not-exist",
},
wantErr: require.Error,
},
{
name: "with odd unclean path through non-existent directory",
cfg: DirectoryConfig{Path: "test-fixtures/does-not-exist/../"},
cfg: Config{Path: "test-fixtures/does-not-exist/../"},
wantErr: require.Error,
},
{
name: "to a file (not a directory)",
cfg: DirectoryConfig{
cfg: Config{
Path: "./test-fixtures/image-simple/Dockerfile",
},
wantErr: require.Error,
},
{
name: "to dir with name and version",
cfg: DirectoryConfig{
cfg: Config{
Path: "./test-fixtures",
Alias: Alias{
Alias: source.Alias{
Name: "name-me-that!",
Version: "version-me-this!",
},
@@ -475,9 +487,9 @@ func Test_DirectorySource_ID(t *testing.T) {
},
{
name: "to different dir with name and version",
cfg: DirectoryConfig{
cfg: Config{
Path: "./test-fixtures/image-simple",
Alias: Alias{
Alias: source.Alias{
Name: "name-me-that!",
Version: "version-me-this!",
},
@@ -487,20 +499,20 @@ func Test_DirectorySource_ID(t *testing.T) {
},
{
name: "with path",
cfg: DirectoryConfig{Path: "./test-fixtures"},
cfg: Config{Path: "./test-fixtures"},
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
},
{
name: "with unclean path",
cfg: DirectoryConfig{Path: "test-fixtures/image-simple/../"},
cfg: Config{Path: "test-fixtures/image-simple/../"},
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
},
{
name: "other fields do not affect ID",
cfg: DirectoryConfig{
cfg: Config{
Path: "test-fixtures",
Base: "a-base!",
Exclude: ExcludeConfig{
Exclude: source.ExcludeConfig{
Paths: []string{"a", "b"},
},
},
@@ -512,7 +524,7 @@ func Test_DirectorySource_ID(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
s, err := NewFromDirectory(tt.cfg)
s, err := New(tt.cfg)
tt.wantErr(t, err)
if err != nil {
return
@@ -523,6 +535,7 @@ func Test_DirectorySource_ID(t *testing.T) {
}
func Test_cleanDirPath(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
abs, err := filepath.Abs("test-fixtures")
require.NoError(t, err)
@@ -12,7 +12,7 @@
// - https://github.com/golang/go/blob/3aea422e2cb8b1ec2e0c2774be97fe96c7299838/src/path/filepath/path_windows.go#L216
// ... which means we can't extract this functionality without build tags.
package source
package directorysource
import (
"testing"
@@ -53,7 +53,7 @@ func Test_DirectorySource_crossPlatformExclusions(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude})
fns, err := GetDirectoryExclusionFunctions(test.root, []string{test.exclude})
require.NoError(t, err)
for _, f := range fns {
+9
View File
@@ -0,0 +1,9 @@
package source
import "github.com/anchore/syft/syft/file"
type FileMetadata struct {
Path string `json:"path" yaml:"path"`
Digests []file.Digest `json:"digests,omitempty" yaml:"digests,omitempty"`
MIMEType string `json:"mimeType" yaml:"mimeType"`
}
@@ -1,4 +1,4 @@
package source
package filesource
import (
"crypto"
@@ -18,27 +18,24 @@ import (
"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/directorysource"
"github.com/anchore/syft/syft/source/internal"
)
var _ Source = (*FileSource)(nil)
var _ source.Source = (*fileSource)(nil)
type FileConfig struct {
type Config struct {
Path string
Exclude ExcludeConfig
Exclude source.ExcludeConfig
DigestAlgorithms []crypto.Hash
Alias Alias
Alias source.Alias
}
type FileSourceMetadata struct {
Path string `json:"path" yaml:"path"`
Digests []file.Digest `json:"digests,omitempty" yaml:"digests,omitempty"`
MIMEType string `json:"mimeType" yaml:"mimeType"`
}
type FileSource struct {
type fileSource struct {
id artifact.ID
digestForVersion string
config FileConfig
config Config
resolver *fileresolver.Directory
mutex *sync.Mutex
closer func() error
@@ -47,7 +44,11 @@ type FileSource struct {
analysisPath string
}
func NewFromFile(cfg FileConfig) (*FileSource, error) {
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)
@@ -83,7 +84,7 @@ func NewFromFile(cfg FileConfig) (*FileSource, error) {
id, versionDigest := deriveIDFromFile(cfg)
return &FileSource{
return &fileSource{
id: id,
config: cfg,
mutex: &sync.Mutex{},
@@ -98,7 +99,7 @@ func NewFromFile(cfg FileConfig) (*FileSource, error) {
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
// logically different, then ID will express that.
func deriveIDFromFile(cfg FileConfig) (artifact.ID, string) {
func deriveIDFromFile(cfg Config) (artifact.ID, string) {
d := digestOfFileContents(cfg.Path)
info := d
@@ -108,14 +109,14 @@ func deriveIDFromFile(cfg FileConfig) (artifact.ID, string) {
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
}
return artifactIDFromDigest(digest.SHA256.FromString(info).String()), d
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d
}
func (s FileSource) ID() artifact.ID {
func (s fileSource) ID() artifact.ID {
return s.id
}
func (s FileSource) Describe() Description {
func (s fileSource) Describe() source.Description {
name := path.Base(s.config.Path)
version := s.digestForVersion
if !s.config.Alias.IsEmpty() {
@@ -128,11 +129,11 @@ func (s FileSource) Describe() Description {
version = a.Version
}
}
return Description{
return source.Description{
ID: string(s.id),
Name: name,
Version: version,
Metadata: FileSourceMetadata{
Metadata: source.FileMetadata{
Path: s.config.Path,
Digests: s.digests,
MIMEType: s.mimeType,
@@ -140,7 +141,7 @@ func (s FileSource) Describe() Description {
}
}
func (s FileSource) FileResolver(_ Scope) (file.Resolver, error) {
func (s fileSource) FileResolver(_ source.Scope) (file.Resolver, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
@@ -148,7 +149,7 @@ func (s FileSource) FileResolver(_ Scope) (file.Resolver, error) {
return s.resolver, nil
}
exclusionFunctions, err := getDirectoryExclusionFunctions(s.analysisPath, s.config.Exclude.Paths)
exclusionFunctions, err := directorysource.GetDirectoryExclusionFunctions(s.analysisPath, s.config.Exclude.Paths)
if err != nil {
return nil, err
}
@@ -221,7 +222,7 @@ func absoluteSymlinkFreePathToParent(path string) (string, error) {
return filepath.Dir(dereferencedAbsAnalysisPath), nil
}
func (s *FileSource) Close() error {
func (s *fileSource) Close() error {
if s.closer == nil {
return nil
}
@@ -0,0 +1,58 @@
package filesource
import (
"context"
"crypto"
"fmt"
"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/anchore/syft/syft/source"
)
func NewSourceProvider(path string, exclude source.ExcludeConfig, digestAlgorithms []crypto.Hash, alias source.Alias) source.Provider {
return &fileSourceProvider{
path: path,
exclude: exclude,
digestAlgorithms: digestAlgorithms,
alias: alias,
}
}
type fileSourceProvider struct {
path string
exclude source.ExcludeConfig
digestAlgorithms []crypto.Hash
alias source.Alias
}
func (p fileSourceProvider) Name() string {
return "local-file"
}
func (p fileSourceProvider) Provide(_ context.Context) (source.Source, error) {
location, err := homedir.Expand(p.path)
if err != nil {
return nil, fmt.Errorf("unable to expand potential directory path: %w", err)
}
fs := afero.NewOsFs()
fileMeta, err := fs.Stat(location)
if err != nil {
return nil, fmt.Errorf("unable to stat location: %w", err)
}
if fileMeta.IsDir() {
return nil, fmt.Errorf("not a file source: %s", p.path)
}
return New(
Config{
Path: location,
Exclude: p.exclude,
DigestAlgorithms: p.digestAlgorithms,
Alias: p.alias,
},
)
}
@@ -1,4 +1,4 @@
package source
package filesource
import (
"io"
@@ -14,9 +14,13 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/testutil"
"github.com/anchore/syft/syft/source"
)
func TestNewFromFile(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct {
desc string
input string
@@ -67,7 +71,7 @@ func TestNewFromFile(t *testing.T) {
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromFile(FileConfig{
src, err := New(Config{
Path: test.input,
})
require.NoError(t, err)
@@ -75,9 +79,9 @@ func TestNewFromFile(t *testing.T) {
require.NoError(t, src.Close())
})
assert.Equal(t, test.input, src.Describe().Metadata.(FileSourceMetadata).Path)
assert.Equal(t, test.input, src.Describe().Metadata.(source.FileMetadata).Path)
res, err := src.FileResolver(SquashedScope)
res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
refs, err := test.testPathFn(res)
@@ -92,6 +96,8 @@ func TestNewFromFile(t *testing.T) {
}
func TestNewFromFile_WithArchive(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct {
desc string
input string
@@ -120,7 +126,7 @@ func TestNewFromFile_WithArchive(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
archivePath := setupArchiveTest(t, test.input, test.layer2)
src, err := NewFromFile(FileConfig{
src, err := New(Config{
Path: archivePath,
})
require.NoError(t, err)
@@ -128,9 +134,9 @@ func TestNewFromFile_WithArchive(t *testing.T) {
require.NoError(t, src.Close())
})
assert.Equal(t, archivePath, src.Describe().Metadata.(FileSourceMetadata).Path)
assert.Equal(t, archivePath, src.Describe().Metadata.(source.FileMetadata).Path)
res, err := src.FileResolver(SquashedScope)
res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
refs, err := res.FilesByPath(test.inputPaths...)
@@ -226,43 +232,45 @@ func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, l
}
func Test_FileSource_ID(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
tests := []struct {
name string
cfg FileConfig
cfg Config
want artifact.ID
wantDigest string
wantErr require.ErrorAssertionFunc
}{
{
name: "empty",
cfg: FileConfig{},
cfg: Config{},
wantErr: require.Error,
},
{
name: "does not exist",
cfg: FileConfig{
cfg: Config{
Path: "./test-fixtures/does-not-exist",
},
wantErr: require.Error,
},
{
name: "to dir",
cfg: FileConfig{
cfg: Config{
Path: "./test-fixtures/image-simple",
},
wantErr: require.Error,
},
{
name: "with path",
cfg: FileConfig{Path: "./test-fixtures/image-simple/Dockerfile"},
cfg: Config{Path: "./test-fixtures/image-simple/Dockerfile"},
want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
},
{
name: "with path and alias",
cfg: FileConfig{
cfg: Config{
Path: "./test-fixtures/image-simple/Dockerfile",
Alias: Alias{
Alias: source.Alias{
Name: "name-me-that!",
Version: "version-me-this!",
},
@@ -272,9 +280,9 @@ func Test_FileSource_ID(t *testing.T) {
},
{
name: "other fields do not affect ID",
cfg: FileConfig{
cfg: Config{
Path: "test-fixtures/image-simple/Dockerfile",
Exclude: ExcludeConfig{
Exclude: source.ExcludeConfig{
Paths: []string{"a", "b"},
},
},
@@ -287,11 +295,12 @@ func Test_FileSource_ID(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
s, err := NewFromFile(tt.cfg)
newSource, err := New(tt.cfg)
tt.wantErr(t, err)
if err != nil {
return
}
s := newSource.(*fileSource)
assert.Equalf(t, tt.want, s.ID(), "ID() mismatch")
assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch")
})
+27
View File
@@ -0,0 +1,27 @@
package source
// ImageMetadata represents all static metadata that defines what a container image is. This is useful to later describe
// "what" was cataloged without needing the more complicated stereoscope Image objects or FileResolver objects.
type ImageMetadata struct {
UserInput string `json:"userInput"`
ID string `json:"imageID"`
ManifestDigest string `json:"manifestDigest"`
MediaType string `json:"mediaType"`
Tags []string `json:"tags"`
Size int64 `json:"imageSize"`
Layers []LayerMetadata `json:"layers"`
RawManifest []byte `json:"manifest"`
RawConfig []byte `json:"config"`
RepoDigests []string `json:"repoDigests"`
Architecture string `json:"architecture"`
Variant string `json:"architectureVariant,omitempty"`
OS string `json:"os"`
Labels map[string]string `json:"labels,omitempty"`
}
// LayerMetadata represents all static metadata that defines what a container image layer is.
type LayerMetadata struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
@@ -1,4 +1,4 @@
package source
package internal
import (
"strings"
@@ -6,6 +6,6 @@ import (
"github.com/anchore/syft/syft/artifact"
)
func artifactIDFromDigest(input string) artifact.ID {
func ArtifactIDFromDigest(input string) artifact.ID {
return artifact.ID(strings.TrimPrefix(input, "sha256:"))
}
+11
View File
@@ -0,0 +1,11 @@
package source
import (
"context"
)
// Provider is able to resolve a source request
type Provider interface {
Name() string
Provide(ctx context.Context) (Source, error)
}
@@ -0,0 +1,56 @@
package sourceproviders
import (
"crypto"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/source"
)
// Config is the uber-configuration for all Syft source providers
type Config struct {
Platform *image.Platform
Alias source.Alias
RegistryOptions *image.RegistryOptions
Exclude source.ExcludeConfig
DigestAlgorithms []crypto.Hash
BasePath string
}
func (c *Config) WithAlias(alias source.Alias) *Config {
c.Alias = alias
return c
}
func (c *Config) WithRegistryOptions(registryOptions *image.RegistryOptions) *Config {
c.RegistryOptions = registryOptions
return c
}
func (c *Config) WithPlatform(platform *image.Platform) *Config {
c.Platform = platform
return c
}
func (c *Config) WithExcludeConfig(excludeConfig source.ExcludeConfig) *Config {
c.Exclude = excludeConfig
return c
}
func (c *Config) WithDigestAlgorithms(algorithms ...crypto.Hash) *Config {
c.DigestAlgorithms = algorithms
return c
}
func (c *Config) WithBasePath(basePath string) *Config {
c.BasePath = basePath
return c
}
func DefaultConfig() *Config {
return &Config{
DigestAlgorithms: []crypto.Hash{
crypto.SHA256,
},
}
}
@@ -0,0 +1,55 @@
package sourceproviders
import (
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/filesource"
"github.com/anchore/syft/syft/source/stereoscopesource"
)
const (
FileTag = stereoscope.FileTag
DirTag = stereoscope.DirTag
PullTag = stereoscope.PullTag
)
// All returns all the configured source providers known to syft
func All(userInput string, cfg *Config) []collections.TaggedValue[source.Provider] {
if cfg == nil {
cfg = DefaultConfig()
}
stereoscopeProviders := stereoscopeSourceProviders(userInput, cfg)
return collections.TaggedValueSet[source.Provider]{}.
// --from file, dir, oci-archive, etc.
Join(stereoscopeProviders.Select(FileTag, DirTag)...).
Join(tagProvider(filesource.NewSourceProvider(userInput, cfg.Exclude, cfg.DigestAlgorithms, cfg.Alias), FileTag)).
Join(tagProvider(directorysource.NewSourceProvider(userInput, cfg.Exclude, cfg.Alias, cfg.BasePath), DirTag)).
// --from docker, registry, etc.
Join(stereoscopeProviders.Select(PullTag)...)
}
func stereoscopeSourceProviders(userInput string, cfg *Config) collections.TaggedValueSet[source.Provider] {
var registry image.RegistryOptions
if cfg.RegistryOptions != nil {
registry = *cfg.RegistryOptions
}
stereoscopeProviders := stereoscopesource.Providers(stereoscopesource.ProviderConfig{
StereoscopeImageProviderConfig: stereoscope.ImageProviderConfig{
UserInput: userInput,
Platform: cfg.Platform,
Registry: registry,
},
Alias: cfg.Alias,
Exclude: cfg.Exclude,
})
return stereoscopeProviders
}
func tagProvider(provider source.Provider, tags ...string) collections.TaggedValue[source.Provider] {
return collections.NewTaggedValue(provider, append([]string{provider.Name()}, tags...)...)
}
-64
View File
@@ -1,64 +0,0 @@
package source
import "github.com/anchore/stereoscope/pkg/image"
// StereoscopeImageSourceMetadata represents all static metadata that defines what a container image is. This is useful to later describe
// "what" was cataloged without needing the more complicated stereoscope Image objects or FileResolver objects.
type StereoscopeImageSourceMetadata struct {
UserInput string `json:"userInput"`
ID string `json:"imageID"`
ManifestDigest string `json:"manifestDigest"`
MediaType string `json:"mediaType"`
Tags []string `json:"tags"`
Size int64 `json:"imageSize"`
Layers []StereoscopeLayerMetadata `json:"layers"`
RawManifest []byte `json:"manifest"`
RawConfig []byte `json:"config"`
RepoDigests []string `json:"repoDigests"`
Architecture string `json:"architecture"`
Variant string `json:"architectureVariant,omitempty"`
OS string `json:"os"`
Labels map[string]string `json:"labels,omitempty"`
}
// StereoscopeLayerMetadata represents all static metadata that defines what a container image layer is.
type StereoscopeLayerMetadata struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
// NewStereoscopeImageMetadata creates a new ImageMetadata object populated from the given stereoscope Image object and user configuration.
func NewStereoscopeImageMetadata(img *image.Image, userInput string) StereoscopeImageSourceMetadata {
// populate artifacts...
tags := make([]string, len(img.Metadata.Tags))
for idx, tag := range img.Metadata.Tags {
tags[idx] = tag.String()
}
theImg := StereoscopeImageSourceMetadata{
ID: img.Metadata.ID,
UserInput: userInput,
ManifestDigest: img.Metadata.ManifestDigest,
Size: img.Metadata.Size,
MediaType: string(img.Metadata.MediaType),
Tags: tags,
Layers: make([]StereoscopeLayerMetadata, len(img.Layers)),
RawConfig: img.Metadata.RawConfig,
RawManifest: img.Metadata.RawManifest,
RepoDigests: img.Metadata.RepoDigests,
Architecture: img.Metadata.Architecture,
Variant: img.Metadata.Variant,
OS: img.Metadata.OS,
Labels: img.Metadata.Config.Config.Labels,
}
// populate image metadata
for idx, l := range img.Layers {
theImg.Layers[idx] = StereoscopeLayerMetadata{
MediaType: string(l.Metadata.MediaType),
Digest: l.Metadata.Digest,
Size: l.Metadata.Size,
}
}
return theImg
}
@@ -1,90 +1,53 @@
package source
package stereoscopesource
import (
"context"
"fmt"
"github.com/bmatcuk/doublestar/v4"
"github.com/distribution/reference"
"github.com/opencontainers/go-digest"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"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 = (*StereoscopeImageSource)(nil)
var _ source.Source = (*stereoscopeImageSource)(nil)
type StereoscopeImageConfig struct {
type ImageConfig struct {
Reference string
From image.Source
Platform *image.Platform
RegistryOptions *image.RegistryOptions
Exclude ExcludeConfig
Alias Alias
Exclude source.ExcludeConfig
Alias source.Alias
}
type StereoscopeImageSource struct {
type stereoscopeImageSource struct {
id artifact.ID
config StereoscopeImageConfig
config ImageConfig
image *image.Image
metadata StereoscopeImageSourceMetadata
metadata source.ImageMetadata
}
func NewFromStereoscopeImageObject(img *image.Image, reference string, alias *Alias) (*StereoscopeImageSource, error) {
var aliasVal Alias
if !alias.IsEmpty() {
aliasVal = *alias
}
cfg := StereoscopeImageConfig{
Reference: reference,
Alias: aliasVal,
}
func New(img *image.Image, cfg ImageConfig) source.Source {
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
return &StereoscopeImageSource{
return &stereoscopeImageSource{
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
config: cfg,
image: img,
metadata: metadata,
}, nil
}
}
func NewFromStereoscopeImage(cfg StereoscopeImageConfig) (*StereoscopeImageSource, error) {
ctx := context.TODO()
var opts []stereoscope.Option
if cfg.RegistryOptions != nil {
opts = append(opts, stereoscope.WithRegistryOptions(*cfg.RegistryOptions))
}
if cfg.Platform != nil {
opts = append(opts, stereoscope.WithPlatform(cfg.Platform.String()))
}
img, err := stereoscope.GetImageFromSource(ctx, cfg.Reference, cfg.From, opts...)
if err != nil {
return nil, fmt.Errorf("unable to load image: %w", err)
}
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
return &StereoscopeImageSource{
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
config: cfg,
image: img,
metadata: metadata,
}, nil
}
func (s StereoscopeImageSource) ID() artifact.ID {
func (s stereoscopeImageSource) ID() artifact.ID {
return s.id
}
func (s StereoscopeImageSource) Describe() Description {
func (s stereoscopeImageSource) Describe() source.Description {
a := s.config.Alias
name := a.Name
@@ -123,7 +86,7 @@ func (s StereoscopeImageSource) Describe() Description {
nameIfUnset(s.metadata.UserInput)
versionIfUnset(s.metadata.ManifestDigest)
return Description{
return source.Description{
ID: string(s.id),
Name: name,
Version: version,
@@ -131,14 +94,14 @@ func (s StereoscopeImageSource) Describe() Description {
}
}
func (s StereoscopeImageSource) FileResolver(scope Scope) (file.Resolver, error) {
func (s stereoscopeImageSource) FileResolver(scope source.Scope) (file.Resolver, error) {
var res file.Resolver
var err error
switch scope {
case SquashedScope:
case source.SquashedScope:
res, err = fileresolver.NewFromContainerImageSquash(s.image)
case AllLayersScope:
case source.AllLayersScope:
res, err = fileresolver.NewFromContainerImageAllLayers(s.image)
default:
return nil, fmt.Errorf("bad image scope provided: %+v", scope)
@@ -156,29 +119,29 @@ func (s StereoscopeImageSource) FileResolver(scope Scope) (file.Resolver, error)
return res, nil
}
func (s StereoscopeImageSource) Close() error {
func (s stereoscopeImageSource) Close() error {
if s.image == nil {
return nil
}
return s.image.Cleanup()
}
func imageMetadataFromStereoscopeImage(img *image.Image, reference string) StereoscopeImageSourceMetadata {
func imageMetadataFromStereoscopeImage(img *image.Image, reference string) source.ImageMetadata {
tags := make([]string, len(img.Metadata.Tags))
for idx, tag := range img.Metadata.Tags {
tags[idx] = tag.String()
}
layers := make([]StereoscopeLayerMetadata, len(img.Layers))
layers := make([]source.LayerMetadata, len(img.Layers))
for idx, l := range img.Layers {
layers[idx] = StereoscopeLayerMetadata{
layers[idx] = source.LayerMetadata{
MediaType: string(l.Metadata.MediaType),
Digest: l.Metadata.Digest,
Size: l.Metadata.Size,
}
}
return StereoscopeImageSourceMetadata{
return source.ImageMetadata{
ID: img.Metadata.ID,
UserInput: reference,
ManifestDigest: img.Metadata.ManifestDigest,
@@ -203,7 +166,7 @@ func imageMetadataFromStereoscopeImage(img *image.Image, reference string) Stere
//
// in all cases, if an alias is provided, it is additionally considered in the ID calculation. This allows for the
// same image to be scanned multiple times with different aliases and be considered logically different.
func deriveIDFromStereoscopeImage(alias Alias, metadata StereoscopeImageSourceMetadata) artifact.ID {
func deriveIDFromStereoscopeImage(alias source.Alias, metadata source.ImageMetadata) artifact.ID {
var input string
if len(metadata.RawManifest) > 0 {
@@ -226,10 +189,10 @@ func deriveIDFromStereoscopeImage(alias Alias, metadata StereoscopeImageSourceMe
input = digest.Canonical.FromString(input + aliasStr).String()
}
return artifactIDFromDigest(input)
return internal.ArtifactIDFromDigest(input)
}
func calculateChainID(lm []StereoscopeLayerMetadata) string {
func calculateChainID(lm []source.LayerMetadata) string {
if len(lm) < 1 {
return ""
}
@@ -242,7 +205,7 @@ func calculateChainID(lm []StereoscopeLayerMetadata) string {
return id
}
func chain(chainID string, layers []StereoscopeLayerMetadata) string {
func chain(chainID string, layers []source.LayerMetadata) string {
if len(layers) < 1 {
return chainID
}
@@ -0,0 +1,61 @@
package stereoscopesource
import (
"context"
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/source"
)
const ImageTag = "image"
type ProviderConfig struct {
StereoscopeImageProviderConfig stereoscope.ImageProviderConfig
Exclude source.ExcludeConfig
Alias source.Alias
}
type stereoscopeImageSourceProvider struct {
stereoscopeProvider image.Provider
cfg ProviderConfig
}
var _ source.Provider = (*stereoscopeImageSourceProvider)(nil)
func (l stereoscopeImageSourceProvider) Name() string {
return l.stereoscopeProvider.Name()
}
func (l stereoscopeImageSourceProvider) Provide(ctx context.Context) (source.Source, error) {
img, err := l.stereoscopeProvider.Provide(ctx)
if err != nil {
return nil, err
}
cfg := ImageConfig{
Reference: l.cfg.StereoscopeImageProviderConfig.UserInput,
Platform: l.cfg.StereoscopeImageProviderConfig.Platform,
RegistryOptions: &l.cfg.StereoscopeImageProviderConfig.Registry,
Exclude: l.cfg.Exclude,
Alias: l.cfg.Alias,
}
if err != nil {
return nil, err
}
return New(img, cfg), nil
}
func Providers(cfg ProviderConfig) []collections.TaggedValue[source.Provider] {
stereoscopeProviders := collections.TaggedValueSet[source.Provider]{}
providers := stereoscope.ImageProviders(cfg.StereoscopeImageProviderConfig)
for _, provider := range providers {
var sourceProvider source.Provider = stereoscopeImageSourceProvider{
stereoscopeProvider: provider.Value,
cfg: cfg,
}
stereoscopeProviders = append(stereoscopeProviders,
collections.NewTaggedValue(sourceProvider, append([]string{provider.Value.Name(), ImageTag}, provider.Tags...)...))
}
return stereoscopeProviders
}
@@ -1,6 +1,7 @@
package source
package stereoscopesource
import (
"context"
"crypto/sha256"
"fmt"
"strings"
@@ -9,12 +10,16 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/internal/testutil"
"github.com/anchore/syft/syft/source"
)
func Test_StereoscopeImage_Exclusions(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct {
desc string
input string
@@ -76,22 +81,27 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromStereoscopeImage(
StereoscopeImageConfig{
Reference: strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1],
From: image.DockerTarballSource,
Exclude: ExcludeConfig{
imageName := strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1]
img, err := stereoscope.GetImage(context.TODO(), imageName)
require.NoError(t, err)
require.NotNil(t, img)
src := New(
img,
ImageConfig{
Reference: imageName,
Exclude: source.ExcludeConfig{
Paths: test.exclusions,
},
},
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
res, err := src.FileResolver(SquashedScope)
res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
contents, err := res.FilesByGlob(test.glob)
@@ -105,15 +115,15 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
func Test_StereoscopeImageSource_ID(t *testing.T) {
tests := []struct {
name string
alias Alias
metadata StereoscopeImageSourceMetadata
alias source.Alias
metadata source.ImageMetadata
want artifact.ID
}{
{
name: "use raw manifest over chain ID or user input",
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{
Layers: []source.LayerMetadata{
{
Digest: "a",
},
@@ -134,9 +144,9 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
},
{
name: "use chain ID over user input",
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
//UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{
Layers: []source.LayerMetadata{
{
Digest: "a",
},
@@ -149,7 +159,7 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
},
},
want: func() artifact.ID {
metadata := []StereoscopeLayerMetadata{
metadata := []source.LayerMetadata{
{
Digest: "a",
},
@@ -165,7 +175,7 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
},
{
name: "use user input last",
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
UserInput: "user-input",
},
want: func() artifact.ID {
@@ -176,9 +186,9 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
},
{
name: "without alias (first)",
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{
Layers: []source.LayerMetadata{
{
Digest: "a",
},
@@ -195,13 +205,13 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
},
{
name: "always consider alias (first)",
alias: Alias{
alias: source.Alias{
Name: "alias",
Version: "version",
},
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{
Layers: []source.LayerMetadata{
{
Digest: "a",
},
@@ -218,18 +228,18 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
},
{
name: "without alias (last)",
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
UserInput: "user-input",
},
want: "ab0dff627d80b9753193d7280bec8f45e8ec6b4cb0912c6fffcf7cd782d9739e",
},
{
name: "always consider alias (last)",
alias: Alias{
alias: source.Alias{
Name: "alias",
Version: "version",
},
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
UserInput: "user-input",
},
want: "fe86c0eecd5654d3c0c0b2176aa394aef6440347c241aa8d9b628dfdde4287cf",
@@ -245,18 +255,18 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
func Test_Describe(t *testing.T) {
tests := []struct {
name string
source StereoscopeImageSource
expected Description
source stereoscopeImageSource
expected source.Description
}{
{
name: "name from user input",
source: StereoscopeImageSource{
source: stereoscopeImageSource{
id: "some-id",
metadata: StereoscopeImageSourceMetadata{
metadata: source.ImageMetadata{
UserInput: "user input",
},
},
expected: Description{
expected: source.Description{
ID: "some-id",
Name: "user input",
},