mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 23:16:30 +01:00
feat: add --from flag, refactor source providers (#2610)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+40
-27
@@ -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)
|
||||
+2
-2
@@ -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 {
|
||||
@@ -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")
|
||||
})
|
||||
@@ -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:"))
|
||||
}
|
||||
@@ -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...)...)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+28
-65
@@ -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
|
||||
}
|
||||
+40
-30
@@ -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",
|
||||
},
|
||||
Reference in New Issue
Block a user