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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 1035 additions and 1080 deletions

View file

@ -122,7 +122,8 @@ syft <image> --scope all-layers
### Supported sources ### Supported sources
Syft can generate an SBOM from a variety of sources: Syft can generate an SBOM from a variety of sources including images, files, directories, and archives. Syft will attempt to
determine the type of source based on provided input, for example:
```bash ```bash
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands) # catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
@ -135,26 +136,24 @@ syft path/to/image.sif
syft path/to/dir syft path/to/dir
``` ```
Sources can be explicitly provided with a scheme: To explicitly specify the source behavior, use the `--from` flag. Allowable options are:
``` ```
docker:yourrepo/yourimage:tag use images from the Docker daemon docker use images from the Docker daemon
podman:yourrepo/yourimage:tag use images from the Podman daemon podman use images from the Podman daemon
containerd:yourrepo/yourimage:tag use images from the Containerd daemon containerd use images from the Containerd daemon
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" docker-archive use a tarball from disk for archives created from "docker save"
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise) oci-archive use a tarball from disk for OCI archives (from Skopeo or otherwise)
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) oci-dir read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk singularity read directly from a Singularity Image Format (SIF) container on disk
dir:path/to/yourproject read directly from a path on disk (any directory) dir read directly from a path on disk (any directory)
file:path/to/yourproject/file read directly from a path on disk (any single file) file read directly from a path on disk (any single file)
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) registry pull image directly from a registry (no container runtime required)
``` ```
If a source is not provided and Syft identifies the input as a potential image reference, Syft will attempt to resolve it using:
the Docker, Podman, and Containerd daemons followed by direct registry access, in that order.
If an image source is not provided and cannot be detected from the given reference it is assumed the image should be pulled from the Docker daemon. This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](#configuration) for more details).
If docker is not present, then the Podman daemon is attempted next, followed by reaching out directly to the image registry last.
This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](https://github.com/anchore/syft#configuration) for more details).
### File selection ### File selection

View file

@ -13,6 +13,7 @@ import (
"github.com/wagoodman/go-progress" "github.com/wagoodman/go-progress"
"github.com/anchore/clio" "github.com/anchore/clio"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/internal/options" "github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui" "github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
@ -26,7 +27,6 @@ import (
"github.com/anchore/syft/syft/format/spdxtagvalue" "github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson" "github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
) )
const ( const (
@ -247,7 +247,11 @@ func predicateType(outputName string) string {
} }
func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) { func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
src, err := getSource(opts, userInput, onlyContainerImages) if len(opts.From) > 1 || (len(opts.From) == 1 && opts.From[0] != stereoscope.RegistryTag) {
return nil, fmt.Errorf("attest requires use of an OCI registry directly, one or more of the specified sources is unsupported: %v", opts.From)
}
src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag)
if err != nil { if err != nil {
return nil, err return nil, err
@ -273,13 +277,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
return s, nil return s, nil
} }
func onlyContainerImages(d *source.Detection) error {
if !d.IsContainerImage() {
return fmt.Errorf("attestations are only supported for oci images at this time")
}
return nil
}
func commandExists(cmd string) bool { func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd) _, err := exec.LookPath(cmd)
return err == nil return err == nil

View file

@ -13,6 +13,8 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/anchore/clio" "github.com/anchore/clio"
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/internal/options" "github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui" "github.com/anchore/syft/cmd/syft/internal/ui"
@ -24,6 +26,7 @@ import (
"github.com/anchore/syft/syft" "github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/sourceproviders"
) )
const ( const (
@ -162,14 +165,23 @@ func validateArgs(cmd *cobra.Command, args []string, error string) error {
return cobra.MaximumNArgs(1)(cmd, args) return cobra.MaximumNArgs(1)(cmd, args)
} }
// nolint:funlen
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error { func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
writer, err := opts.SBOMWriter() writer, err := opts.SBOMWriter()
if err != nil { if err != nil {
return err return err
} }
src, err := getSource(&opts.Catalog, userInput) sources := opts.From
if len(sources) == 0 {
// extract a scheme if it matches any provider tag; this is a holdover for compatibility, using the --from flag is recommended
explicitSource, newUserInput := stereoscope.ExtractSchemeSource(userInput, allSourceProviderTags()...)
if explicitSource != "" {
sources = append(sources, explicitSource)
userInput = newUserInput
}
}
src, err := getSource(ctx, &opts.Catalog, userInput, sources...)
if err != nil { if err != nil {
return err return err
@ -199,23 +211,21 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
return nil return nil
} }
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) { func getSource(ctx context.Context, opts *options.Catalog, userInput string, sources ...string) (source.Source, error) {
detection, err := source.Detect( cfg := syft.DefaultGetSourceConfig().
userInput, WithRegistryOptions(opts.Registry.ToOptions()).
source.DetectConfig{ WithAlias(source.Alias{
DefaultImageSource: opts.Source.Image.DefaultPullSource, Name: opts.Source.Name,
}, Version: opts.Source.Version,
) }).
if err != nil { WithExcludeConfig(source.ExcludeConfig{
return nil, fmt.Errorf("could not deteremine source: %w", err) Paths: opts.Exclusions,
} }).
WithBasePath(opts.Source.BasePath).
for _, filter := range filters { WithSources(sources...).
if err := filter(detection); err != nil { WithDefaultImagePullSource(opts.Source.Image.DefaultPullSource)
return nil, err
}
}
var err error
var platform *image.Platform var platform *image.Platform
if opts.Platform != "" { if opts.Platform != "" {
@ -223,29 +233,22 @@ func getSource(opts *options.Catalog, userInput string, filters ...func(*source.
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid platform: %w", err) return nil, fmt.Errorf("invalid platform: %w", err)
} }
cfg = cfg.WithPlatform(platform)
} }
hashers, err := file.Hashers(opts.Source.File.Digests...) if opts.Source.File.Digests != nil {
hashers, err := file.Hashers(opts.Source.File.Digests...)
if err != nil {
return nil, fmt.Errorf("invalid hash algorithm: %w", err)
}
cfg = cfg.WithDigestAlgorithms(hashers...)
}
src, err := syft.GetSource(ctx, userInput, cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid hash algorithm: %w", err) return nil, fmt.Errorf("could not determine source: %w", err)
} }
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: opts.Source.BasePath,
},
)
if err != nil { if err != nil {
if userInput == "power-user" { if userInput == "power-user" {
bus.Notify("Note: the 'power-user' command has been removed.") bus.Notify("Note: the 'power-user' command has been removed.")
@ -445,3 +448,7 @@ func getHintPhrase(expErr task.ErrInvalidExpression) string {
func trimOperation(x string) string { func trimOperation(x string) string {
return strings.TrimLeft(x, "+-") return strings.TrimLeft(x, "+-")
} }
func allSourceProviderTags() []string {
return collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All("", nil)...).Tags()
}

View file

@ -46,6 +46,7 @@ type Catalog struct {
// configuration for the source (the subject being analyzed) // configuration for the source (the subject being analyzed)
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"` Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
From []string `yaml:"from" json:"from" mapstructure:"from"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"` Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
@ -163,6 +164,9 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&cfg.Scope, "scope", "s", flags.StringVarP(&cfg.Scope, "scope", "s",
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues)) fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))
flags.StringArrayVarP(&cfg.From, "from", "",
"specify the source behavior to use (e.g. docker, registry, oci-dir, ...)")
flags.StringVarP(&cfg.Platform, "platform", "", flags.StringVarP(&cfg.Platform, "platform", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')") "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
@ -220,6 +224,8 @@ func (cfg *Catalog) PostLoad() error {
return out return out
} }
cfg.From = flatten(cfg.From)
cfg.Catalogers = flatten(cfg.Catalogers) cfg.Catalogers = flatten(cfg.Catalogers)
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers) cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers) cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)

View file

@ -6,6 +6,8 @@ import (
"strings" "strings"
"github.com/scylladb/go-set/strset" "github.com/scylladb/go-set/strset"
"github.com/anchore/syft/syft/source/sourceproviders"
) )
type sourceConfig struct { type sourceConfig struct {
@ -25,12 +27,13 @@ type imageSource struct {
} }
func defaultSourceConfig() sourceConfig { func defaultSourceConfig() sourceConfig {
var digests []string
for _, alg := range sourceproviders.DefaultConfig().DigestAlgorithms {
digests = append(digests, alg.String())
}
return sourceConfig{ return sourceConfig{
File: fileSource{ File: fileSource{
Digests: []string{"sha256"}, Digests: digests,
},
Image: imageSource{
DefaultPullSource: "",
}, },
} }
} }

View file

@ -22,15 +22,11 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
tarPath := imagetest.GetFixtureImageTarPath(b, fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(b, fixtureImageName)
// get the source object for the image // get the source object for the image
userInput := "docker-archive:" + tarPath theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
require.NoError(b, err)
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
require.NoError(b, err) require.NoError(b, err)
b.Cleanup(func() { b.Cleanup(func() {
theSource.Close() require.NoError(b, theSource.Close())
}) })
// build the SBOM // build the SBOM

View file

@ -34,17 +34,13 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
// get the fixture image tar file // get the fixture image tar file
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
userInput := "docker-archive:" + tarPath
// get the source to build an SBOM against // get the source to build an SBOM against
detection, err := source.Detect(userInput, source.DefaultDetectConfig()) theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
theSource.Close() require.NoError(t, theSource.Close())
}) })
s, err := syft.CreateSBOM(context.Background(), theSource, cfg) s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
@ -71,14 +67,10 @@ func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMCo
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag) cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)
// get the source to build an sbom against // get the source to build an sbom against
userInput := "dir:" + dir theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
require.NoError(t, err)
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
theSource.Close() require.NoError(t, theSource.Close())
}) })
// build the SBOM // build the SBOM

View file

@ -44,17 +44,7 @@ func imageReference() string {
func getSource(input string) source.Source { func getSource(input string) source.Source {
fmt.Println("detecting source type for input:", input, "...") fmt.Println("detecting source type for input:", input, "...")
detection, err := source.Detect(input, src, err := syft.GetSource(context.Background(), input, nil)
source.DetectConfig{
DefaultImageSource: "docker",
},
)
if err != nil {
panic(err)
}
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
if err != nil { if err != nil {
panic(err) panic(err)

View file

@ -37,17 +37,7 @@ func imageReference() string {
} }
func getSource(input string) source.Source { func getSource(input string) source.Source {
detection, err := source.Detect(input, src, err := syft.GetSource(context.Background(), input, nil)
source.DetectConfig{
DefaultImageSource: "docker",
},
)
if err != nil {
panic(err)
}
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
if err != nil { if err != nil {
panic(err) panic(err)

View file

@ -41,17 +41,7 @@ func imageReference() string {
} }
func getSource(input string) source.Source { func getSource(input string) source.Source {
detection, err := source.Detect(input, src, err := syft.GetSource(context.Background(), input, nil)
source.DetectConfig{
DefaultImageSource: "docker",
},
)
if err != nil {
panic(err)
}
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
if err != nil { if err != nil {
panic(err) panic(err)

View file

@ -1,10 +1,15 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"os" "os"
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/sourceproviders"
) )
/* /*
@ -28,18 +33,18 @@ import (
const defaultImage = "alpine:3.19" const defaultImage = "alpine:3.19"
func main() { func main() {
detection, err := source.Detect( userInput := imageReference()
imageReference(),
source.DetectConfig{
DefaultImageSource: "docker",
},
)
if err != nil { // parse the scheme against the known set of schemes
panic(err) schemeSource, newUserInput := stereoscope.ExtractSchemeSource(userInput, allSourceTags()...)
// set up the GetSourceConfig
getSourceCfg := syft.DefaultGetSourceConfig()
if schemeSource != "" {
getSourceCfg = getSourceCfg.WithSources(schemeSource)
userInput = newUserInput
} }
src, err := syft.GetSource(context.Background(), userInput, getSourceCfg)
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
if err != nil { if err != nil {
panic(err) panic(err)
@ -60,3 +65,7 @@ func imageReference() string {
} }
return defaultImage return defaultImage
} }
func allSourceTags() []string {
return collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All("", nil)...).Tags()
}

View file

@ -1,11 +1,13 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"os" "os"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope"
"github.com/anchore/syft/syft/source" "github.com/anchore/stereoscope/pkg/image/oci"
"github.com/anchore/syft/syft/source/stereoscopesource"
) )
/* /*
@ -16,22 +18,15 @@ import (
const defaultImage = "alpine:3.19" const defaultImage = "alpine:3.19"
func main() { func main() {
platform, err := image.NewPlatform("linux/amd64") // using oci.Registry causes the lookup to always use the registry, there are several other "Source" options here
img, err := stereoscope.GetImageFromSource(context.Background(), imageReference(), oci.Registry, stereoscope.WithPlatform("linux/amd64"))
if err != nil { if err != nil {
panic(err) panic(err)
} }
src, err := source.NewFromStereoscopeImage( src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
source.StereoscopeImageConfig{ Reference: imageReference(),
Reference: imageReference(), })
From: image.OciRegistrySource, // always use the registry, there are several other "Source" options here
Platform: platform,
},
)
if err != nil {
panic(err)
}
// Show a basic description of the source to the screen // Show a basic description of the source to the screen
enc := json.NewEncoder(os.Stdout) enc := json.NewEncoder(os.Stdout)

4
go.mod
View file

@ -16,7 +16,7 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426 github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426
github.com/anchore/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5 github.com/anchore/stereoscope v0.0.2-0.20240221144950-cf0e754f5b56
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
// we are hinting brotli to latest due to warning when installing archiver v3: // we are hinting brotli to latest due to warning when installing archiver v3:
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption // go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
@ -80,6 +80,8 @@ require (
modernc.org/sqlite v1.29.2 modernc.org/sqlite v1.29.2
) )
require github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect

6
go.sum
View file

@ -97,6 +97,8 @@ github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65 h1:u9XrEabKlGPsrmRvAE
github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65/go.mod h1:8Jr7CjmwFVcBPtkJdTpaAGHimoGJGfbExypjzOu87Og= github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65/go.mod h1:8Jr7CjmwFVcBPtkJdTpaAGHimoGJGfbExypjzOu87Og=
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b h1:L/djgY7ZbZ/38+wUtdkk398W3PIBJLkt1N8nU/7e47A= github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b h1:L/djgY7ZbZ/38+wUtdkk398W3PIBJLkt1N8nU/7e47A=
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b/go.mod h1:TLcE0RE5+8oIx2/NPWem/dq1DeaMoC+fPEH7hoSzPLo= github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b/go.mod h1:TLcE0RE5+8oIx2/NPWem/dq1DeaMoC+fPEH7hoSzPLo=
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 h1:GjNGuwK5jWjJMyVppBjYS54eOiiSNv4Ba869k4wh72Q=
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8=
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw= github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw=
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
@ -109,8 +111,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426 h1:agoiZchSf1Nnnos1azwIg5hk5Ao9TzZNBD9++AChGEg= github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426 h1:agoiZchSf1Nnnos1azwIg5hk5Ao9TzZNBD9++AChGEg=
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
github.com/anchore/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5 h1:o//fhRcSpOYHC/xG/HiI6ddtSMiRgHlB96xJQXZawZM= github.com/anchore/stereoscope v0.0.2-0.20240221144950-cf0e754f5b56 h1:iHvTXZA+qEozPGRRuW1Mv7r7w2fHeJdzWDx+YsSIbyg=
github.com/anchore/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5/go.mod h1:o0TqYkefad6kIPtmbigFKss7P48z4bjd8Vp5Wklbf3Y= github.com/anchore/stereoscope v0.0.2-0.20240221144950-cf0e754f5b56/go.mod h1:evQiJMQG56Z7/L5uhA8kfhhjF6ESJUZzUH9ms6bQ2Co=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=

View file

@ -345,9 +345,9 @@ func (c *CreateSBOMConfig) Create(ctx context.Context, src source.Source) (*sbom
func findDefaultTag(src source.Description) (string, error) { func findDefaultTag(src source.Description) (string, error) {
switch m := src.Metadata.(type) { switch m := src.Metadata.(type) {
case source.StereoscopeImageSourceMetadata: case source.ImageMetadata:
return pkgcataloging.ImageTag, nil return pkgcataloging.ImageTag, nil
case source.FileSourceMetadata, source.DirectorySourceMetadata: case source.FileMetadata, source.DirectoryMetadata:
return pkgcataloging.DirectoryTag, nil return pkgcataloging.DirectoryTag, nil
default: default:
return "", fmt.Errorf("unable to determine default cataloger tag for source type=%T", m) return "", fmt.Errorf("unable to determine default cataloger tag for source type=%T", m)

View file

@ -62,15 +62,15 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
} }
imgSrc := source.Description{ imgSrc := source.Description{
Metadata: source.StereoscopeImageSourceMetadata{}, Metadata: source.ImageMetadata{},
} }
dirSrc := source.Description{ dirSrc := source.Description{
Metadata: source.DirectorySourceMetadata{}, Metadata: source.DirectoryMetadata{},
} }
fileSrc := source.Description{ fileSrc := source.Description{
Metadata: source.FileSourceMetadata{}, Metadata: source.FileMetadata{},
} }
tests := []struct { tests := []struct {
@ -437,21 +437,21 @@ func Test_findDefaultTag(t *testing.T) {
{ {
name: "image", name: "image",
src: source.Description{ src: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{}, Metadata: source.ImageMetadata{},
}, },
want: pkgcataloging.ImageTag, want: pkgcataloging.ImageTag,
}, },
{ {
name: "directory", name: "directory",
src: source.Description{ src: source.Description{
Metadata: source.DirectorySourceMetadata{}, Metadata: source.DirectoryMetadata{},
}, },
want: pkgcataloging.DirectoryTag, want: pkgcataloging.DirectoryTag,
}, },
{ {
name: "file", name: "file",
src: source.Description{ src: source.Description{
Metadata: source.FileSourceMetadata{}, Metadata: source.FileMetadata{},
}, },
want: pkgcataloging.DirectoryTag, // not a mistake... want: pkgcataloging.DirectoryTag, // not a mistake...
}, },

View file

@ -17,6 +17,8 @@ import (
intFile "github.com/anchore/syft/internal/file" intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/stereoscopesource"
) )
func testDigests(t testing.TB, root string, files []string, hashes ...crypto.Hash) map[file.Coordinates][]file.Digest { func testDigests(t testing.TB, root string, files []string, hashes ...crypto.Hash) map[file.Coordinates][]file.Digest {
@ -77,7 +79,7 @@ func TestDigestsCataloger(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
c := NewCataloger(test.digests) c := NewCataloger(test.digests)
src, err := source.NewFromDirectoryPath("test-fixtures/last/") src, err := directorysource.NewFromPath("test-fixtures/last/")
require.NoError(t, err) require.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
@ -96,10 +98,9 @@ func TestDigestsCataloger_MixFileTypes(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", testImage) img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil) src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
if err != nil { Reference: testImage,
t.Fatalf("could not create source: %+v", err) })
}
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
if err != nil { if err != nil {
@ -169,8 +170,9 @@ func TestFileDigestCataloger_GivenCoordinates(t *testing.T) {
c := NewCataloger([]crypto.Hash{crypto.SHA256}) c := NewCataloger([]crypto.Hash{crypto.SHA256})
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil) src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
require.NoError(t, err) Reference: testImage,
})
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)

View file

@ -12,6 +12,7 @@ import (
"github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/stereoscopesource"
) )
func TestFileMetadataCataloger(t *testing.T) { func TestFileMetadataCataloger(t *testing.T) {
@ -21,8 +22,9 @@ func TestFileMetadataCataloger(t *testing.T) {
c := NewCataloger() c := NewCataloger()
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil) src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
require.NoError(t, err) Reference: testImage,
})
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)
@ -159,8 +161,9 @@ func TestFileMetadataCataloger_GivenCoordinates(t *testing.T) {
c := NewCataloger() c := NewCataloger()
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil) src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
require.NoError(t, err) Reference: testImage,
})
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)

View file

@ -12,6 +12,8 @@ import (
"github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/stereoscopesource"
) )
func Test_allRegularFiles(t *testing.T) { func Test_allRegularFiles(t *testing.T) {
@ -28,8 +30,9 @@ func Test_allRegularFiles(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", testImage) img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
s, err := source.NewFromStereoscopeImageObject(img, testImage, nil) s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
require.NoError(t, err) Reference: testImage,
})
r, err := s.FileResolver(source.SquashedScope) r, err := s.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)
@ -42,7 +45,7 @@ func Test_allRegularFiles(t *testing.T) {
{ {
name: "directory", name: "directory",
setup: func() file.Resolver { setup: func() file.Resolver {
s, err := source.NewFromDirectoryPath("test-fixtures/symlinked-root/nested/link-root") s, err := directorysource.NewFromPath("test-fixtures/symlinked-root/nested/link-root")
require.NoError(t, err) require.NoError(t, err)
r, err := s.FileResolver(source.SquashedScope) r, err := s.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)

View file

@ -208,7 +208,7 @@ func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependenc
} }
func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property { func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property {
metadata, ok := srcMetadata.Metadata.(source.StereoscopeImageSourceMetadata) metadata, ok := srcMetadata.Metadata.(source.ImageMetadata)
if ok { if ok {
props := helpers.EncodeProperties(metadata.Labels, "syft:image:labels") props := helpers.EncodeProperties(metadata.Labels, "syft:image:labels")
return &props return &props
@ -220,7 +220,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
name := srcMetadata.Name name := srcMetadata.Name
version := srcMetadata.Version version := srcMetadata.Version
switch metadata := srcMetadata.Metadata.(type) { switch metadata := srcMetadata.Metadata.(type) {
case source.StereoscopeImageSourceMetadata: case source.ImageMetadata:
if name == "" { if name == "" {
name = metadata.UserInput name = metadata.UserInput
} }
@ -237,7 +237,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
Name: name, Name: name,
Version: version, Version: version,
} }
case source.DirectorySourceMetadata: case source.DirectoryMetadata:
if name == "" { if name == "" {
name = metadata.Path name = metadata.Path
} }
@ -252,7 +252,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
Name: name, Name: name,
Version: version, Version: version,
} }
case source.FileSourceMetadata: case source.FileMetadata:
if name == "" { if name == "" {
name = metadata.Path name = metadata.Path
} }

View file

@ -160,7 +160,7 @@ func Test_toBomDescriptor(t *testing.T) {
name: "test-image", name: "test-image",
version: "1.0.0", version: "1.0.0",
srcMetadata: source.Description{ srcMetadata: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
Labels: map[string]string{ Labels: map[string]string{
"key1": "value1", "key1": "value1",
}, },

View file

@ -181,7 +181,7 @@ func toRootPackage(s source.Description) *spdx.Package {
purpose := "" purpose := ""
var checksums []spdx.Checksum var checksums []spdx.Checksum
switch m := s.Metadata.(type) { switch m := s.Metadata.(type) {
case source.StereoscopeImageSourceMetadata: case source.ImageMetadata:
prefix = prefixImage prefix = prefixImage
purpose = spdxPrimaryPurposeContainer purpose = spdxPrimaryPurposeContainer
@ -211,11 +211,11 @@ func toRootPackage(s source.Description) *spdx.Package {
} }
} }
case source.DirectorySourceMetadata: case source.DirectoryMetadata:
prefix = prefixDirectory prefix = prefixDirectory
purpose = spdxPrimaryPurposeFile purpose = spdxPrimaryPurposeFile
case source.FileSourceMetadata: case source.FileMetadata:
prefix = prefixFile prefix = prefixFile
purpose = spdxPrimaryPurposeFile purpose = spdxPrimaryPurposeFile

View file

@ -35,7 +35,7 @@ func Test_toFormatModel(t *testing.T) {
Source: source.Description{ Source: source.Description{
Name: "alpine", Name: "alpine",
Version: "sha256:d34db33f", Version: "sha256:d34db33f",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "alpine:latest", UserInput: "alpine:latest",
ManifestDigest: "sha256:d34db33f", ManifestDigest: "sha256:d34db33f",
}, },
@ -106,7 +106,7 @@ func Test_toFormatModel(t *testing.T) {
in: sbom.SBOM{ in: sbom.SBOM{
Source: source.Description{ Source: source.Description{
Name: "some/directory", Name: "some/directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/directory", Path: "some/directory",
}, },
}, },
@ -170,7 +170,7 @@ func Test_toFormatModel(t *testing.T) {
Source: source.Description{ Source: source.Description{
Name: "path/to/some.file", Name: "path/to/some.file",
Version: "sha256:d34db33f", Version: "sha256:d34db33f",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "path/to/some.file", Path: "path/to/some.file",
Digests: []file.Digest{ Digests: []file.Digest{
{ {

View file

@ -146,7 +146,7 @@ func containerSource(p *spdx.Package) source.Description {
ID: id, ID: id,
Name: p.PackageName, Name: p.PackageName,
Version: p.PackageVersion, Version: p.PackageVersion,
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: container, UserInput: container,
ID: id, ID: id,
Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool
@ -187,7 +187,7 @@ func fileSource(p *spdx.Package) source.Description {
func fileSourceMetadata(p *spdx.Package) (any, string) { func fileSourceMetadata(p *spdx.Package) (any, string) {
version := p.PackageVersion version := p.PackageVersion
m := source.FileSourceMetadata{ m := source.FileMetadata{
Path: p.PackageName, Path: p.PackageName,
} }
// if this is a Syft SBOM, we might have output a digest as the version // if this is a Syft SBOM, we might have output a digest as the version
@ -206,7 +206,7 @@ func fileSourceMetadata(p *spdx.Package) (any, string) {
} }
func directorySourceMetadata(p *spdx.Package) (any, string) { func directorySourceMetadata(p *spdx.Package) (any, string) {
return source.DirectorySourceMetadata{ return source.DirectoryMetadata{
Path: p.PackageName, Path: p.PackageName,
Base: "", Base: "",
}, p.PackageVersion }, p.PackageVersion
@ -229,15 +229,15 @@ func extractSourceFromNamespace(ns string) source.Description {
switch p { switch p {
case helpers.InputFile: case helpers.InputFile:
return source.Description{ return source.Description{
Metadata: source.FileSourceMetadata{}, Metadata: source.FileMetadata{},
} }
case helpers.InputImage: case helpers.InputImage:
return source.Description{ return source.Description{
Metadata: source.StereoscopeImageSourceMetadata{}, Metadata: source.ImageMetadata{},
} }
case helpers.InputDirectory: case helpers.InputDirectory:
return source.Description{ return source.Description{
Metadata: source.DirectorySourceMetadata{}, Metadata: source.DirectoryMetadata{},
} }
} }
} }

View file

@ -199,15 +199,15 @@ func TestExtractSourceFromNamespaces(t *testing.T) {
}{ }{
{ {
namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3", namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3",
expected: source.FileSourceMetadata{}, expected: source.FileMetadata{},
}, },
{ {
namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3", namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3",
expected: source.StereoscopeImageSourceMetadata{}, expected: source.ImageMetadata{},
}, },
{ {
namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3", namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3",
expected: source.DirectorySourceMetadata{}, expected: source.DirectoryMetadata{},
}, },
{ {
namespace: "https://another-host/blob/123", namespace: "https://another-host/blob/123",
@ -460,7 +460,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
name: "image source", name: "image source",
source: source.Description{ source: source.Description{
ID: "DocumentRoot-Image-some-image", ID: "DocumentRoot-Image-some-image",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
ID: "DocumentRoot-Image-some-image", ID: "DocumentRoot-Image-some-image",
UserInput: "some-image:some-tag", UserInput: "some-image:some-tag",
ManifestDigest: "sha256:ab8b83234bc28f28d8e", ManifestDigest: "sha256:ab8b83234bc28f28d8e",
@ -476,7 +476,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
source: source.Description{ source: source.Description{
ID: "DocumentRoot-Directory-.", ID: "DocumentRoot-Directory-.",
Name: ".", Name: ".",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: ".", Path: ".",
}, },
}, },
@ -488,7 +488,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
source: source.Description{ source: source.Description{
ID: "DocumentRoot-Directory-my-app", ID: "DocumentRoot-Directory-my-app",
Name: "my-app", Name: "my-app",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "my-app", Path: "my-app",
}, },
}, },
@ -499,7 +499,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
name: "file source", name: "file source",
source: source.Description{ source: source.Description{
ID: "DocumentRoot-File-my-app.exe", ID: "DocumentRoot-File-my-app.exe",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "my-app.exe", Path: "my-app.exe",
Digests: []file.Digest{ Digests: []file.Digest{
{ {

View file

@ -116,16 +116,16 @@ func toPath(s source.Description, p pkg.Package) string {
} }
packagePath = strings.TrimPrefix(packagePath, "/") packagePath = strings.TrimPrefix(packagePath, "/")
switch metadata := s.Metadata.(type) { switch metadata := s.Metadata.(type) {
case source.StereoscopeImageSourceMetadata: case source.ImageMetadata:
image := strings.ReplaceAll(metadata.UserInput, ":/", "//") image := strings.ReplaceAll(metadata.UserInput, ":/", "//")
return fmt.Sprintf("%s:/%s", image, packagePath) return fmt.Sprintf("%s:/%s", image, packagePath)
case source.FileSourceMetadata: case source.FileMetadata:
path := trimRelative(metadata.Path) path := trimRelative(metadata.Path)
if isArchive(metadata.Path) { if isArchive(metadata.Path) {
return fmt.Sprintf("%s:/%s", path, packagePath) return fmt.Sprintf("%s:/%s", path, packagePath)
} }
return path return path
case source.DirectorySourceMetadata: case source.DirectoryMetadata:
path := trimRelative(metadata.Path) path := trimRelative(metadata.Path)
if path != "" { if path != "" {
return fmt.Sprintf("%s/%s", path, packagePath) return fmt.Sprintf("%s/%s", path, packagePath)

View file

@ -22,7 +22,7 @@ func sbomFixture() sbom.SBOM {
Name: "syft", Name: "syft",
}, },
Source: source.Description{ Source: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "ubuntu:18.04", UserInput: "ubuntu:18.04",
Architecture: "amd64", Architecture: "amd64",
}, },
@ -150,27 +150,27 @@ func Test_toGithubModel(t *testing.T) {
}, },
{ {
name: "current directory", name: "current directory",
metadata: source.DirectorySourceMetadata{Path: "."}, metadata: source.DirectoryMetadata{Path: "."},
testPath: "etc", testPath: "etc",
}, },
{ {
name: "relative directory", name: "relative directory",
metadata: source.DirectorySourceMetadata{Path: "./artifacts"}, metadata: source.DirectoryMetadata{Path: "./artifacts"},
testPath: "artifacts/etc", testPath: "artifacts/etc",
}, },
{ {
name: "absolute directory", name: "absolute directory",
metadata: source.DirectorySourceMetadata{Path: "/artifacts"}, metadata: source.DirectoryMetadata{Path: "/artifacts"},
testPath: "/artifacts/etc", testPath: "/artifacts/etc",
}, },
{ {
name: "file", name: "file",
metadata: source.FileSourceMetadata{Path: "./executable"}, metadata: source.FileMetadata{Path: "./executable"},
testPath: "executable", testPath: "executable",
}, },
{ {
name: "archive", name: "archive",
metadata: source.FileSourceMetadata{Path: "./archive.tar.gz"}, metadata: source.FileMetadata{Path: "./archive.tar.gz"},
testPath: "archive.tar.gz:/etc", testPath: "archive.tar.gz:/etc",
}, },
} }

View file

@ -222,7 +222,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
ID: "", ID: "",
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet) // TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: c.Name, UserInput: c.Name,
ID: c.BOMRef, ID: c.BOMRef,
ManifestDigest: c.Version, ManifestDigest: c.Version,
@ -235,7 +235,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
// TODO: this is lossy... we can't know if this is a file or a directory // TODO: this is lossy... we can't know if this is a file or a directory
return source.Description{ return source.Description{
ID: "", ID: "",
Metadata: source.FileSourceMetadata{Path: c.Name}, Metadata: source.FileMetadata{Path: c.Name},
} }
} }
return source.Description{} return source.Description{}

View file

@ -10,11 +10,11 @@ func DocumentName(src source.Description) string {
} }
switch metadata := src.Metadata.(type) { switch metadata := src.Metadata.(type) {
case source.StereoscopeImageSourceMetadata: case source.ImageMetadata:
return metadata.UserInput return metadata.UserInput
case source.DirectorySourceMetadata: case source.DirectoryMetadata:
return metadata.Path return metadata.Path
case source.FileSourceMetadata: case source.FileMetadata:
return metadata.Path return metadata.Path
default: default:
return "unknown" return "unknown"

View file

@ -23,7 +23,7 @@ func Test_DocumentName(t *testing.T) {
{ {
name: "image", name: "image",
srcMetadata: source.Description{ srcMetadata: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "image-repo/name:tag", UserInput: "image-repo/name:tag",
ID: "id", ID: "id",
ManifestDigest: "digest", ManifestDigest: "digest",
@ -34,14 +34,14 @@ func Test_DocumentName(t *testing.T) {
{ {
name: "directory", name: "directory",
srcMetadata: source.Description{ srcMetadata: source.Description{
Metadata: source.DirectorySourceMetadata{Path: "some/path/to/place"}, Metadata: source.DirectoryMetadata{Path: "some/path/to/place"},
}, },
expected: "some/path/to/place", expected: "some/path/to/place",
}, },
{ {
name: "file", name: "file",
srcMetadata: source.Description{ srcMetadata: source.Description{
Metadata: source.FileSourceMetadata{Path: "some/path/to/place"}, Metadata: source.FileMetadata{Path: "some/path/to/place"},
}, },
expected: "some/path/to/place", expected: "some/path/to/place",
}, },
@ -49,7 +49,7 @@ func Test_DocumentName(t *testing.T) {
name: "named", name: "named",
srcMetadata: source.Description{ srcMetadata: source.Description{
Name: "some/name", Name: "some/name",
Metadata: source.FileSourceMetadata{Path: "some/path/to/place"}, Metadata: source.FileMetadata{Path: "some/path/to/place"},
}, },
expected: "some/name", expected: "some/name",
}, },

View file

@ -27,11 +27,11 @@ func DocumentNamespace(name string, src source.Description, desc sbom.Descriptor
name = cleanName(name) name = cleanName(name)
input := "unknown-source-type" input := "unknown-source-type"
switch src.Metadata.(type) { switch src.Metadata.(type) {
case source.StereoscopeImageSourceMetadata: case source.ImageMetadata:
input = InputImage input = InputImage
case source.DirectorySourceMetadata: case source.DirectoryMetadata:
input = InputDirectory input = InputDirectory
case source.FileSourceMetadata: case source.FileMetadata:
input = InputFile input = InputFile
} }

View file

@ -25,7 +25,7 @@ func Test_documentNamespace(t *testing.T) {
name: "image", name: "image",
inputName: "my-name", inputName: "my-name",
src: source.Description{ src: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "image-repo/name:tag", UserInput: "image-repo/name:tag",
ID: "id", ID: "id",
ManifestDigest: "digest", ManifestDigest: "digest",
@ -37,7 +37,7 @@ func Test_documentNamespace(t *testing.T) {
name: "directory", name: "directory",
inputName: "my-name", inputName: "my-name",
src: source.Description{ src: source.Description{
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path/to/place", Path: "some/path/to/place",
}, },
}, },
@ -47,7 +47,7 @@ func Test_documentNamespace(t *testing.T) {
name: "file", name: "file",
inputName: "my-name", inputName: "my-name",
src: source.Description{ src: source.Description{
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path/to/place", Path: "some/path/to/place",
}, },
}, },

View file

@ -12,7 +12,7 @@ import (
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source/directorysource"
) )
func DirectoryInput(t testing.TB, dir string) sbom.SBOM { func DirectoryInput(t testing.TB, dir string) sbom.SBOM {
@ -22,8 +22,8 @@ func DirectoryInput(t testing.TB, dir string) sbom.SBOM {
require.NoError(t, os.MkdirAll(path, 0755)) require.NoError(t, os.MkdirAll(path, 0755))
src, err := source.NewFromDirectory( src, err := directorysource.New(
source.DirectoryConfig{ directorysource.Config{
Path: path, Path: path,
Base: dir, Base: dir,
}, },
@ -63,8 +63,8 @@ func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM {
require.NoError(t, os.MkdirAll(path, 0755)) require.NoError(t, os.MkdirAll(path, 0755))
src, err := source.NewFromDirectory( src, err := directorysource.New(
source.DirectoryConfig{ directorysource.Config{
Path: path, Path: path,
Base: dir, Base: dir,
}, },

View file

@ -5,7 +5,6 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/filetree"
@ -16,7 +15,7 @@ import (
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source/stereoscopesource"
) )
func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM { func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM {
@ -42,8 +41,9 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
// this is a hard coded value that is not given by the fixture helper and must be provided manually // this is a hard coded value that is not given by the fixture helper and must be provided manually
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
src, err := source.NewFromStereoscopeImageObject(img, "user-image-input", nil) src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
assert.NoError(t, err) Reference: "user-image-input",
})
return sbom.SBOM{ return sbom.SBOM{
Artifacts: sbom.Artifacts{ Artifacts: sbom.Artifacts{

View file

@ -72,7 +72,7 @@ func TestSPDXJSONSPDXIDs(t *testing.T) {
Relationships: nil, Relationships: nil,
Source: source.Description{ Source: source.Description{
Name: "foobar/baz", // in this case, foobar is used as the spdx document name Name: "foobar/baz", // in this case, foobar is used as the spdx document name
Metadata: source.DirectorySourceMetadata{}, Metadata: source.DirectoryMetadata{},
}, },
Descriptor: sbom.Descriptor{ Descriptor: sbom.Descriptor{
Name: "syft", Name: "syft",

View file

@ -263,7 +263,7 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
ID: "some-id", ID: "some-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "/some-file-source-path", Path: "/some-file-source-path",
Digests: []file.Digest{ Digests: []file.Digest{
{ {

View file

@ -260,7 +260,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
}, },
Source: source.Description{ Source: source.Description{
ID: "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", ID: "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-image-input", UserInput: "user-image-input",
ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
@ -269,7 +269,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b", "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b",
}, },
Size: 38, Size: 38,
Layers: []source.StereoscopeLayerMetadata{ Layers: []source.LayerMetadata{
{ {
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",

View file

@ -89,7 +89,7 @@ func extractPreSchemaV9Metadata(t string, target []byte) (interface{}, error) {
cleanTarget = string(target) cleanTarget = string(target)
} }
return source.DirectorySourceMetadata{ return source.DirectoryMetadata{
Path: cleanTarget, Path: cleanTarget,
}, nil }, nil
@ -99,12 +99,12 @@ func extractPreSchemaV9Metadata(t string, target []byte) (interface{}, error) {
cleanTarget = string(target) cleanTarget = string(target)
} }
return source.FileSourceMetadata{ return source.FileMetadata{
Path: cleanTarget, Path: cleanTarget,
}, nil }, nil
case "image": case "image":
var payload source.StereoscopeImageSourceMetadata var payload source.ImageMetadata
if err := json.Unmarshal(target, &payload); err != nil { if err := json.Unmarshal(target, &payload); err != nil {
return nil, err return nil, err
} }

View file

@ -32,7 +32,7 @@ func TestSource_UnmarshalJSON(t *testing.T) {
expected: &Source{ expected: &Source{
ID: "foobar", ID: "foobar",
Type: "directory", Type: "directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "/var/lib/foo", Path: "/var/lib/foo",
//Base: "/nope", // note: should be ignored entirely //Base: "/nope", // note: should be ignored entirely
}, },
@ -67,14 +67,14 @@ func TestSource_UnmarshalJSON(t *testing.T) {
expected: &Source{ expected: &Source{
ID: "foobar", ID: "foobar",
Type: "image", Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "alpine:3.10", UserInput: "alpine:3.10",
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a", ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c", ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
MediaType: "application/vnd.docker.distribution.manifest.v2+json", MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Tags: []string{}, Tags: []string{},
Size: 5576169, Size: 5576169,
Layers: []source.StereoscopeLayerMetadata{ Layers: []source.LayerMetadata{
{ {
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
@ -124,7 +124,7 @@ func TestSource_UnmarshalJSON(t *testing.T) {
expected: &Source{ expected: &Source{
ID: "foobar", ID: "foobar",
Type: "file", Type: "file",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "/var/lib/foo/go.mod", Path: "/var/lib/foo/go.mod",
Digests: []file.Digest{ Digests: []file.Digest{
{ {
@ -188,7 +188,7 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
expectedSource: &Source{ expectedSource: &Source{
ID: "foobar", ID: "foobar",
Type: "directory", Type: "directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "/var/lib/foo", Path: "/var/lib/foo",
}, },
}, },
@ -204,7 +204,7 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
expectedSource: &Source{ expectedSource: &Source{
ID: "foobar", ID: "foobar",
Type: "directory", Type: "directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "/var/lib/foo", Path: "/var/lib/foo",
}, },
}, },
@ -239,14 +239,14 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
expectedSource: &Source{ expectedSource: &Source{
ID: "foobar", ID: "foobar",
Type: "image", Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "alpine:3.10", UserInput: "alpine:3.10",
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a", ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c", ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
MediaType: "application/vnd.docker.distribution.manifest.v2+json", MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Tags: []string{}, Tags: []string{},
Size: 5576169, Size: 5576169,
Layers: []source.StereoscopeLayerMetadata{ Layers: []source.LayerMetadata{
{ {
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
@ -288,7 +288,7 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
expectedSource: &Source{ expectedSource: &Source{
ID: "foobar", ID: "foobar",
Type: "file", Type: "file",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "/var/lib/foo/go.mod", Path: "/var/lib/foo/go.mod",
}, },
}, },

View file

@ -303,7 +303,7 @@ func toSourceModel(src source.Description) model.Source {
Metadata: src.Metadata, Metadata: src.Metadata,
} }
if metadata, ok := src.Metadata.(source.StereoscopeImageSourceMetadata); ok { if metadata, ok := src.Metadata.(source.ImageMetadata); ok {
// ensure that empty collections are not shown as null // ensure that empty collections are not shown as null
if metadata.RepoDigests == nil { if metadata.RepoDigests == nil {
metadata.RepoDigests = []string{} metadata.RepoDigests = []string{}

View file

@ -26,7 +26,7 @@ func Test_toSourceModel_IgnoreBase(t *testing.T) {
name: "directory", name: "directory",
src: source.Description{ src: source.Description{
ID: "test-id", ID: "test-id",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -59,7 +59,7 @@ func Test_toSourceModel(t *testing.T) {
ID: "test-id", ID: "test-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -69,7 +69,7 @@ func Test_toSourceModel(t *testing.T) {
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Type: "directory", Type: "directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -81,7 +81,7 @@ func Test_toSourceModel(t *testing.T) {
ID: "test-id", ID: "test-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -92,7 +92,7 @@ func Test_toSourceModel(t *testing.T) {
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Type: "file", Type: "file",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -105,7 +105,7 @@ func Test_toSourceModel(t *testing.T) {
ID: "test-id", ID: "test-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",
@ -117,7 +117,7 @@ func Test_toSourceModel(t *testing.T) {
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Type: "image", Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",
@ -133,7 +133,7 @@ func Test_toSourceModel(t *testing.T) {
name: "directory - no name/version", name: "directory - no name/version",
src: source.Description{ src: source.Description{
ID: "test-id", ID: "test-id",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -141,7 +141,7 @@ func Test_toSourceModel(t *testing.T) {
expected: model.Source{ expected: model.Source{
ID: "test-id", ID: "test-id",
Type: "directory", Type: "directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -151,7 +151,7 @@ func Test_toSourceModel(t *testing.T) {
name: "file - no name/version", name: "file - no name/version",
src: source.Description{ src: source.Description{
ID: "test-id", ID: "test-id",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -160,7 +160,7 @@ func Test_toSourceModel(t *testing.T) {
expected: model.Source{ expected: model.Source{
ID: "test-id", ID: "test-id",
Type: "file", Type: "file",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -171,7 +171,7 @@ func Test_toSourceModel(t *testing.T) {
name: "image - no name/version", name: "image - no name/version",
src: source.Description{ src: source.Description{
ID: "test-id", ID: "test-id",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",
@ -181,7 +181,7 @@ func Test_toSourceModel(t *testing.T) {
expected: model.Source{ expected: model.Source{
ID: "test-id", ID: "test-id",
Type: "image", Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",

View file

@ -35,7 +35,7 @@ func Test_toSyftSourceData(t *testing.T) {
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Type: "directory", Type: "directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -44,7 +44,7 @@ func Test_toSyftSourceData(t *testing.T) {
ID: "the-id", ID: "the-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -57,7 +57,7 @@ func Test_toSyftSourceData(t *testing.T) {
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Type: "file", Type: "file",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -67,7 +67,7 @@ func Test_toSyftSourceData(t *testing.T) {
ID: "the-id", ID: "the-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -81,7 +81,7 @@ func Test_toSyftSourceData(t *testing.T) {
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Type: "image", Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",
@ -92,7 +92,7 @@ func Test_toSyftSourceData(t *testing.T) {
ID: "the-id", ID: "the-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",
@ -107,14 +107,14 @@ func Test_toSyftSourceData(t *testing.T) {
src: model.Source{ src: model.Source{
ID: "the-id", ID: "the-id",
Type: "directory", Type: "directory",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
}, },
expected: &source.Description{ expected: &source.Description{
ID: "the-id", ID: "the-id",
Metadata: source.DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
}, },
@ -125,7 +125,7 @@ func Test_toSyftSourceData(t *testing.T) {
src: model.Source{ src: model.Source{
ID: "the-id", ID: "the-id",
Type: "file", Type: "file",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -133,7 +133,7 @@ func Test_toSyftSourceData(t *testing.T) {
}, },
expected: &source.Description{ expected: &source.Description{
ID: "the-id", ID: "the-id",
Metadata: source.FileSourceMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain", MIMEType: "text/plain",
@ -145,7 +145,7 @@ func Test_toSyftSourceData(t *testing.T) {
src: model.Source{ src: model.Source{
ID: "the-id", ID: "the-id",
Type: "image", Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",
@ -154,7 +154,7 @@ func Test_toSyftSourceData(t *testing.T) {
}, },
expected: &source.Description{ expected: &source.Description{
ID: "the-id", ID: "the-id",
Metadata: source.StereoscopeImageSourceMetadata{ Metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
ID: "id...", ID: "id...",
ManifestDigest: "digest...", ManifestDigest: "digest...",
@ -178,7 +178,7 @@ func Test_idsHaveChanged(t *testing.T) {
s := toSyftModel(model.Document{ s := toSyftModel(model.Document{
Source: model.Source{ Source: model.Source{
Type: "file", Type: "file",
Metadata: source.FileSourceMetadata{Path: "some/path"}, Metadata: source.FileMetadata{Path: "some/path"},
}, },
Artifacts: []model.Package{ Artifacts: []model.Package{
{ {

View file

@ -38,11 +38,11 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
w.Init(writer, 0, 8, 0, '\t', tabwriter.AlignRight) w.Init(writer, 0, 8, 0, '\t', tabwriter.AlignRight)
switch metadata := s.Source.Metadata.(type) { switch metadata := s.Source.Metadata.(type) {
case source.DirectorySourceMetadata: case source.DirectoryMetadata:
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path) fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
case source.FileSourceMetadata: case source.FileMetadata:
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path) fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
case source.StereoscopeImageSourceMetadata: case source.ImageMetadata:
fmt.Fprintln(w, "[Image]") fmt.Fprintln(w, "[Image]")
for idx, l := range metadata.Layers { for idx, l := range metadata.Layers {

101
syft/get_source.go Normal file
View file

@ -0,0 +1,101 @@
package syft
import (
"context"
"errors"
"fmt"
"os"
"github.com/anchore/syft/syft/source"
)
// GetSource uses all of Syft's known source providers to attempt to resolve the user input to a usable source.Source
func GetSource(ctx context.Context, userInput string, cfg *GetSourceConfig) (source.Source, error) {
if cfg == nil {
cfg = DefaultGetSourceConfig()
}
providers, err := cfg.getProviders(userInput)
if err != nil {
return nil, err
}
var errs []error
var fileNotfound error
// call each source provider until we find a valid source
for _, p := range providers {
src, err := p.Provide(ctx)
if err != nil {
err = eachError(err, func(err error) error {
if errors.Is(err, os.ErrNotExist) {
fileNotfound = err
return nil
}
return err
})
if err != nil {
errs = append(errs, err)
}
}
if src != nil {
// if we have a non-image type and platform is specified, it's an error
if cfg.SourceProviderConfig.Platform != nil {
meta := src.Describe().Metadata
switch meta.(type) {
case *source.ImageMetadata, source.ImageMetadata:
default:
return src, fmt.Errorf("platform specified with non-image source")
}
}
return src, nil
}
}
if fileNotfound != nil {
errs = append([]error{fileNotfound}, errs...)
}
return nil, sourceError(userInput, errs...)
}
func sourceError(userInput string, errs ...error) error {
switch len(errs) {
case 0:
return nil
case 1:
return fmt.Errorf("an error occurred attempting to resolve '%s': %w", userInput, errs[0])
}
errorTexts := ""
for _, e := range errs {
errorTexts += fmt.Sprintf("\n - %s", e)
}
return fmt.Errorf("errors occurred attempting to resolve '%s':%s", userInput, errorTexts)
}
func eachError(err error, fn func(error) error) error {
out := fn(err)
// unwrap singly wrapped errors
if e, ok := err.(interface {
Unwrap() error
}); ok {
wrapped := e.Unwrap()
got := eachError(wrapped, fn)
// return the outer error if received the same wrapped error
if errors.Is(got, wrapped) {
return err
}
return got
}
// unwrap errors from errors.Join
if errs, ok := err.(interface {
Unwrap() []error
}); ok {
for _, e := range errs.Unwrap() {
e = eachError(e, fn)
if e != nil {
out = errors.Join(out, e)
}
}
}
return out
}

91
syft/get_source_config.go Normal file
View file

@ -0,0 +1,91 @@
package syft
import (
"crypto"
"fmt"
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/sourceproviders"
)
type GetSourceConfig struct {
// SourceProviderConfig may optionally be provided to be used when constructing the default set of source providers, unused if All specified
SourceProviderConfig *sourceproviders.Config
// Sources is an explicit list of source names to use, in order, to attempt to locate a source
Sources []string
// DefaultImagePullSource will cause a particular image pull source to be used as the first pull source, followed by other pull sources
DefaultImagePullSource string
}
func (c *GetSourceConfig) WithAlias(alias source.Alias) *GetSourceConfig {
c.SourceProviderConfig = c.SourceProviderConfig.WithAlias(alias)
return c
}
func (c *GetSourceConfig) WithRegistryOptions(registryOptions *image.RegistryOptions) *GetSourceConfig {
c.SourceProviderConfig = c.SourceProviderConfig.WithRegistryOptions(registryOptions)
return c
}
func (c *GetSourceConfig) WithPlatform(platform *image.Platform) *GetSourceConfig {
c.SourceProviderConfig = c.SourceProviderConfig.WithPlatform(platform)
return c
}
func (c *GetSourceConfig) WithExcludeConfig(excludeConfig source.ExcludeConfig) *GetSourceConfig {
c.SourceProviderConfig = c.SourceProviderConfig.WithExcludeConfig(excludeConfig)
return c
}
func (c *GetSourceConfig) WithDigestAlgorithms(algorithms ...crypto.Hash) *GetSourceConfig {
c.SourceProviderConfig = c.SourceProviderConfig.WithDigestAlgorithms(algorithms...)
return c
}
func (c *GetSourceConfig) WithBasePath(basePath string) *GetSourceConfig {
c.SourceProviderConfig = c.SourceProviderConfig.WithBasePath(basePath)
return c
}
func (c *GetSourceConfig) WithSources(sources ...string) *GetSourceConfig {
c.Sources = sources
return c
}
func (c *GetSourceConfig) WithDefaultImagePullSource(defaultImagePullSource string) *GetSourceConfig {
c.DefaultImagePullSource = defaultImagePullSource
return c
}
func (c *GetSourceConfig) getProviders(userInput string) ([]source.Provider, error) {
providers := collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All(userInput, c.SourceProviderConfig)...)
// if the "default image pull source" is set, we move this as the first pull source
if c.DefaultImagePullSource != "" {
base := providers.Remove(sourceproviders.PullTag)
pull := providers.Select(sourceproviders.PullTag)
def := pull.Select(c.DefaultImagePullSource)
if len(def) == 0 {
return nil, fmt.Errorf("invalid DefaultImagePullSource: %s; available values are: %v", c.DefaultImagePullSource, pull.Tags())
}
providers = base.Join(def...).Join(pull...)
}
// narrow the sources to those explicitly requested generally by a user
if len(c.Sources) > 0 {
// select the explicitly provided sources, in order
providers = providers.Select(c.Sources...)
}
return providers.Values(), nil
}
func DefaultGetSourceConfig() *GetSourceConfig {
return &GetSourceConfig{
SourceProviderConfig: sourceproviders.DefaultConfig(),
}
}

View file

@ -6,5 +6,5 @@ import "github.com/anchore/syft/syft/source"
// AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field). // AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).
func AllTypes() []any { func AllTypes() []any {
return []any{source.DirectorySourceMetadata{}, source.FileSourceMetadata{}, source.StereoscopeImageSourceMetadata{}} return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}}
} }

View file

@ -8,9 +8,9 @@ import (
) )
var jsonNameFromType = map[reflect.Type][]string{ var jsonNameFromType = map[reflect.Type][]string{
reflect.TypeOf(source.DirectorySourceMetadata{}): {"directory", "dir"}, reflect.TypeOf(source.DirectoryMetadata{}): {"directory", "dir"},
reflect.TypeOf(source.FileSourceMetadata{}): {"file"}, reflect.TypeOf(source.FileMetadata{}): {"file"},
reflect.TypeOf(source.StereoscopeImageSourceMetadata{}): {"image"}, reflect.TypeOf(source.ImageMetadata{}): {"image"},
} }
func AllTypeNames() []string { func AllTypeNames() []string {

View file

@ -0,0 +1,26 @@
package testutil
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func Chdir(t *testing.T, dir string) {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("unable to get working directory: %v", err)
}
err = os.Chdir(dir)
if err != nil {
t.Fatalf("unable to chdir to '%s': %v", dir, err)
}
t.Cleanup(func() {
require.NoError(t, os.Chdir(wd))
})
}

View file

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
) )
func TestIdentifyRelease(t *testing.T) { func TestIdentifyRelease(t *testing.T) {
@ -336,7 +337,9 @@ func TestIdentifyRelease(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) { t.Run(test.fixture, func(t *testing.T) {
s, err := source.NewFromDirectoryPath(test.fixture) s, err := directorysource.New(directorysource.Config{
Path: test.fixture,
})
require.NoError(t, err) require.NoError(t, err)
resolver, err := s.FileResolver(source.SquashedScope) resolver, err := s.FileResolver(source.SquashedScope)

View file

@ -21,6 +21,8 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil" "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/stereoscopesource"
) )
var mustUseOriginalBinaries = flag.Bool("must-use-original-binaries", false, "force the use of binaries for testing (instead of snippets)") var mustUseOriginalBinaries = flag.Bool("must-use-original-binaries", false, "force the use of binaries for testing (instead of snippets)")
@ -896,7 +898,7 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
// full binaries are tested (no snippets), and if no binary is found the test will be skipped. // full binaries are tested (no snippets), and if no binary is found the test will be skipped.
path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries) path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries)
src, err := source.NewFromDirectoryPath(path) src, err := directorysource.NewFromPath(path)
require.NoError(t, err) require.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
@ -936,8 +938,9 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig()) c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage) img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
src, err := source.NewFromStereoscopeImageObject(img, test.fixtureImage, nil) src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
require.NoError(t, err) Reference: test.fixtureImage,
})
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)
@ -966,7 +969,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) { func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig()) c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
src, err := source.NewFromDirectoryPath("test-fixtures/classifiers/negative") src, err := directorysource.NewFromPath("test-fixtures/classifiers/negative")
assert.NoError(t, err) assert.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)
@ -1080,7 +1083,7 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
c := NewClassifierCataloger(test.config) c := NewClassifierCataloger(test.config)
src, err := source.NewFromDirectoryPath(test.fixtureDir) src, err := directorysource.NewFromPath(test.fixtureDir)
require.NoError(t, err) require.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope) resolver, err := src.FileResolver(source.SquashedScope)

View file

@ -22,6 +22,8 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic" "github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/stereoscopesource"
) )
type locationComparer func(x, y file.Location) bool type locationComparer func(x, y file.Location) bool
@ -88,7 +90,7 @@ func DefaultLicenseComparer(x, y pkg.License) bool {
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester { func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
t.Helper() t.Helper()
s, err := source.NewFromDirectoryPath(path) s, err := directorysource.NewFromPath(path)
require.NoError(t, err) require.NoError(t, err)
resolver, err := s.FileResolver(source.AllLayersScope) resolver, err := s.FileResolver(source.AllLayersScope)
@ -152,8 +154,9 @@ func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *Cat
t.Helper() t.Helper()
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName) img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
s, err := source.NewFromStereoscopeImageObject(img, fixtureName, nil) s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
require.NoError(t, err) Reference: fixtureName,
})
r, err := s.FileResolver(source.SquashedScope) r, err := s.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)

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
}
}

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")
})
}
}

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)
}

View file

@ -1,4 +1,4 @@
package source package directorysource
import ( import (
"fmt" "fmt"
@ -14,37 +14,34 @@ import (
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver" "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 Path string
Base string Base string
Exclude ExcludeConfig Exclude source.ExcludeConfig
Alias Alias Alias source.Alias
} }
type DirectorySourceMetadata struct { type directorySource 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 {
id artifact.ID id artifact.ID
config DirectoryConfig config Config
resolver *fileresolver.Directory resolver *fileresolver.Directory
mutex *sync.Mutex mutex *sync.Mutex
} }
func NewFromDirectoryPath(path string) (*DirectorySource, error) { func NewFromPath(path string) (source.Source, error) {
cfg := DirectoryConfig{ cfg := Config{
Path: path, 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) fi, err := os.Stat(cfg.Path)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err) 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 nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
} }
return &DirectorySource{ return &directorySource{
id: deriveIDFromDirectory(cfg), id: deriveIDFromDirectory(cfg),
config: cfg, config: cfg,
mutex: &sync.Mutex{}, 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 // 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 // are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
// consideration for the path. // consideration for the path.
func deriveIDFromDirectory(cfg DirectoryConfig) artifact.ID { func deriveIDFromDirectory(cfg Config) artifact.ID {
var info string var info string
if !cfg.Alias.IsEmpty() { if !cfg.Alias.IsEmpty() {
// don't use any of the path information -- instead use the alias name and version as the artifact ID. // 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) 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 { func cleanDirPath(path, base string) string {
@ -108,11 +105,11 @@ func cleanDirPath(path, base string) string {
return path return path
} }
func (s DirectorySource) ID() artifact.ID { func (s directorySource) ID() artifact.ID {
return s.id return s.id
} }
func (s DirectorySource) Describe() Description { func (s directorySource) Describe() source.Description {
name := cleanDirPath(s.config.Path, s.config.Base) name := cleanDirPath(s.config.Path, s.config.Base)
version := "" version := ""
if !s.config.Alias.IsEmpty() { if !s.config.Alias.IsEmpty() {
@ -124,23 +121,23 @@ func (s DirectorySource) Describe() Description {
version = a.Version version = a.Version
} }
} }
return Description{ return source.Description{
ID: string(s.id), ID: string(s.id),
Name: name, Name: name,
Version: version, Version: version,
Metadata: DirectorySourceMetadata{ Metadata: source.DirectoryMetadata{
Path: s.config.Path, Path: s.config.Path,
Base: s.config.Base, Base: s.config.Base,
}, },
} }
} }
func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) { func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.resolver == nil { 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 { if err != nil {
return nil, err return nil, err
} }
@ -156,14 +153,14 @@ func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) {
return s.resolver, nil return s.resolver, nil
} }
func (s *DirectorySource) Close() error { func (s *directorySource) Close() error {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
s.resolver = nil s.resolver = nil
return 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 { if len(exclusions) == 0 {
return nil, nil return nil, nil
} }

View file

@ -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
}

View file

@ -1,4 +1,4 @@
package source package directorysource
import ( import (
"io/fs" "io/fs"
@ -13,9 +13,13 @@ import (
"github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/internal/fileresolver" "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) { func TestNewFromDirectory(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
@ -54,7 +58,7 @@ func TestNewFromDirectory(t *testing.T) {
if test.cxErr == nil { if test.cxErr == nil {
test.cxErr = require.NoError test.cxErr = require.NoError
} }
src, err := NewFromDirectory(DirectoryConfig{ src, err := New(Config{
Path: test.input, Path: test.input,
}) })
test.cxErr(t, err) test.cxErr(t, err)
@ -65,9 +69,9 @@ func TestNewFromDirectory(t *testing.T) {
t.Cleanup(func() { t.Cleanup(func() {
require.NoError(t, src.Close()) 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) require.NoError(t, err)
refs, err := res.FilesByPath(test.inputPaths...) refs, err := res.FilesByPath(test.inputPaths...)
@ -82,6 +86,8 @@ func TestNewFromDirectory(t *testing.T) {
} }
func Test_DirectorySource_FilesByGlob(t *testing.T) { func Test_DirectorySource_FilesByGlob(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
@ -109,10 +115,10 @@ func Test_DirectorySource_FilesByGlob(t *testing.T) {
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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) require.NoError(t, err)
res, err := src.FileResolver(SquashedScope) res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
require.NoError(t, src.Close()) require.NoError(t, src.Close())
@ -129,6 +135,8 @@ func Test_DirectorySource_FilesByGlob(t *testing.T) {
} }
func Test_DirectorySource_Exclusions(t *testing.T) { func Test_DirectorySource_Exclusions(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
@ -270,9 +278,9 @@ func Test_DirectorySource_Exclusions(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(DirectoryConfig{ src, err := New(Config{
Path: test.input, Path: test.input,
Exclude: ExcludeConfig{ Exclude: source.ExcludeConfig{
Paths: test.exclusions, Paths: test.exclusions,
}, },
}) })
@ -282,13 +290,13 @@ func Test_DirectorySource_Exclusions(t *testing.T) {
}) })
if test.err { if test.err {
_, err = src.FileResolver(SquashedScope) _, err = src.FileResolver(source.SquashedScope)
require.Error(t, err) require.Error(t, err)
return return
} }
require.NoError(t, err) require.NoError(t, err)
res, err := src.FileResolver(SquashedScope) res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)
locations, err := res.FilesByGlob(test.glob) locations, err := res.FilesByGlob(test.glob)
@ -388,7 +396,7 @@ func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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) require.NoError(t, err)
for _, f := range fns { for _, f := range fns {
@ -400,6 +408,8 @@ func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
} }
func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) { func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
@ -414,13 +424,13 @@ func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
require.NoError(t, src.Close()) require.NoError(t, src.Close())
}) })
res, err := src.FileResolver(SquashedScope) res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)
refs, err := res.FilesByPath(test.path) refs, err := res.FilesByPath(test.path)
@ -432,41 +442,43 @@ func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
} }
func Test_DirectorySource_ID(t *testing.T) { func Test_DirectorySource_ID(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
tests := []struct { tests := []struct {
name string name string
cfg DirectoryConfig cfg Config
want artifact.ID want artifact.ID
wantErr require.ErrorAssertionFunc wantErr require.ErrorAssertionFunc
}{ }{
{ {
name: "empty", name: "empty",
cfg: DirectoryConfig{}, cfg: Config{},
wantErr: require.Error, wantErr: require.Error,
}, },
{ {
name: "to a non-existent directory", name: "to a non-existent directory",
cfg: DirectoryConfig{ cfg: Config{
Path: "./test-fixtures/does-not-exist", Path: "./test-fixtures/does-not-exist",
}, },
wantErr: require.Error, wantErr: require.Error,
}, },
{ {
name: "with odd unclean path through non-existent directory", 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, wantErr: require.Error,
}, },
{ {
name: "to a file (not a directory)", name: "to a file (not a directory)",
cfg: DirectoryConfig{ cfg: Config{
Path: "./test-fixtures/image-simple/Dockerfile", Path: "./test-fixtures/image-simple/Dockerfile",
}, },
wantErr: require.Error, wantErr: require.Error,
}, },
{ {
name: "to dir with name and version", name: "to dir with name and version",
cfg: DirectoryConfig{ cfg: Config{
Path: "./test-fixtures", Path: "./test-fixtures",
Alias: Alias{ Alias: source.Alias{
Name: "name-me-that!", Name: "name-me-that!",
Version: "version-me-this!", Version: "version-me-this!",
}, },
@ -475,9 +487,9 @@ func Test_DirectorySource_ID(t *testing.T) {
}, },
{ {
name: "to different dir with name and version", name: "to different dir with name and version",
cfg: DirectoryConfig{ cfg: Config{
Path: "./test-fixtures/image-simple", Path: "./test-fixtures/image-simple",
Alias: Alias{ Alias: source.Alias{
Name: "name-me-that!", Name: "name-me-that!",
Version: "version-me-this!", Version: "version-me-this!",
}, },
@ -487,20 +499,20 @@ func Test_DirectorySource_ID(t *testing.T) {
}, },
{ {
name: "with path", name: "with path",
cfg: DirectoryConfig{Path: "./test-fixtures"}, cfg: Config{Path: "./test-fixtures"},
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"), want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
}, },
{ {
name: "with unclean path", name: "with unclean path",
cfg: DirectoryConfig{Path: "test-fixtures/image-simple/../"}, cfg: Config{Path: "test-fixtures/image-simple/../"},
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"), want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
}, },
{ {
name: "other fields do not affect ID", name: "other fields do not affect ID",
cfg: DirectoryConfig{ cfg: Config{
Path: "test-fixtures", Path: "test-fixtures",
Base: "a-base!", Base: "a-base!",
Exclude: ExcludeConfig{ Exclude: source.ExcludeConfig{
Paths: []string{"a", "b"}, Paths: []string{"a", "b"},
}, },
}, },
@ -512,7 +524,7 @@ func Test_DirectorySource_ID(t *testing.T) {
if tt.wantErr == nil { if tt.wantErr == nil {
tt.wantErr = require.NoError tt.wantErr = require.NoError
} }
s, err := NewFromDirectory(tt.cfg) s, err := New(tt.cfg)
tt.wantErr(t, err) tt.wantErr(t, err)
if err != nil { if err != nil {
return return
@ -523,6 +535,7 @@ func Test_DirectorySource_ID(t *testing.T) {
} }
func Test_cleanDirPath(t *testing.T) { func Test_cleanDirPath(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
abs, err := filepath.Abs("test-fixtures") abs, err := filepath.Abs("test-fixtures")
require.NoError(t, err) require.NoError(t, err)

View file

@ -12,7 +12,7 @@
// - https://github.com/golang/go/blob/3aea422e2cb8b1ec2e0c2774be97fe96c7299838/src/path/filepath/path_windows.go#L216 // - 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. // ... which means we can't extract this functionality without build tags.
package source package directorysource
import ( import (
"testing" "testing"
@ -53,7 +53,7 @@ func Test_DirectorySource_crossPlatformExclusions(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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) require.NoError(t, err)
for _, f := range fns { for _, f := range fns {

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"`
}

View file

@ -1,4 +1,4 @@
package source package filesource
import ( import (
"crypto" "crypto"
@ -18,27 +18,24 @@ import (
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver" "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 Path string
Exclude ExcludeConfig Exclude source.ExcludeConfig
DigestAlgorithms []crypto.Hash DigestAlgorithms []crypto.Hash
Alias Alias Alias source.Alias
} }
type FileSourceMetadata struct { type fileSource 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 {
id artifact.ID id artifact.ID
digestForVersion string digestForVersion string
config FileConfig config Config
resolver *fileresolver.Directory resolver *fileresolver.Directory
mutex *sync.Mutex mutex *sync.Mutex
closer func() error closer func() error
@ -47,7 +44,11 @@ type FileSource struct {
analysisPath string 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) fileMeta, err := os.Stat(cfg.Path)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err) 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) id, versionDigest := deriveIDFromFile(cfg)
return &FileSource{ return &fileSource{
id: id, id: id,
config: cfg, config: cfg,
mutex: &sync.Mutex{}, 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 // 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 // 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. // 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) d := digestOfFileContents(cfg.Path)
info := d info := d
@ -108,14 +109,14 @@ func deriveIDFromFile(cfg FileConfig) (artifact.ID, string) {
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version) 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 return s.id
} }
func (s FileSource) Describe() Description { func (s fileSource) Describe() source.Description {
name := path.Base(s.config.Path) name := path.Base(s.config.Path)
version := s.digestForVersion version := s.digestForVersion
if !s.config.Alias.IsEmpty() { if !s.config.Alias.IsEmpty() {
@ -128,11 +129,11 @@ func (s FileSource) Describe() Description {
version = a.Version version = a.Version
} }
} }
return Description{ return source.Description{
ID: string(s.id), ID: string(s.id),
Name: name, Name: name,
Version: version, Version: version,
Metadata: FileSourceMetadata{ Metadata: source.FileMetadata{
Path: s.config.Path, Path: s.config.Path,
Digests: s.digests, Digests: s.digests,
MIMEType: s.mimeType, 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() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
@ -148,7 +149,7 @@ func (s FileSource) FileResolver(_ Scope) (file.Resolver, error) {
return s.resolver, nil 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 { if err != nil {
return nil, err return nil, err
} }
@ -221,7 +222,7 @@ func absoluteSymlinkFreePathToParent(path string) (string, error) {
return filepath.Dir(dereferencedAbsAnalysisPath), nil return filepath.Dir(dereferencedAbsAnalysisPath), nil
} }
func (s *FileSource) Close() error { func (s *fileSource) Close() error {
if s.closer == nil { if s.closer == nil {
return nil return nil
} }

View file

@ -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,
},
)
}

View file

@ -1,4 +1,4 @@
package source package filesource
import ( import (
"io" "io"
@ -14,9 +14,13 @@ import (
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/testutil"
"github.com/anchore/syft/syft/source"
) )
func TestNewFromFile(t *testing.T) { func TestNewFromFile(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
@ -67,7 +71,7 @@ func TestNewFromFile(t *testing.T) {
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
src, err := NewFromFile(FileConfig{ src, err := New(Config{
Path: test.input, Path: test.input,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -75,9 +79,9 @@ func TestNewFromFile(t *testing.T) {
require.NoError(t, src.Close()) 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) require.NoError(t, err)
refs, err := test.testPathFn(res) refs, err := test.testPathFn(res)
@ -92,6 +96,8 @@ func TestNewFromFile(t *testing.T) {
} }
func TestNewFromFile_WithArchive(t *testing.T) { func TestNewFromFile_WithArchive(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
@ -120,7 +126,7 @@ func TestNewFromFile_WithArchive(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
archivePath := setupArchiveTest(t, test.input, test.layer2) archivePath := setupArchiveTest(t, test.input, test.layer2)
src, err := NewFromFile(FileConfig{ src, err := New(Config{
Path: archivePath, Path: archivePath,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -128,9 +134,9 @@ func TestNewFromFile_WithArchive(t *testing.T) {
require.NoError(t, src.Close()) 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) require.NoError(t, err)
refs, err := res.FilesByPath(test.inputPaths...) 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) { func Test_FileSource_ID(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
tests := []struct { tests := []struct {
name string name string
cfg FileConfig cfg Config
want artifact.ID want artifact.ID
wantDigest string wantDigest string
wantErr require.ErrorAssertionFunc wantErr require.ErrorAssertionFunc
}{ }{
{ {
name: "empty", name: "empty",
cfg: FileConfig{}, cfg: Config{},
wantErr: require.Error, wantErr: require.Error,
}, },
{ {
name: "does not exist", name: "does not exist",
cfg: FileConfig{ cfg: Config{
Path: "./test-fixtures/does-not-exist", Path: "./test-fixtures/does-not-exist",
}, },
wantErr: require.Error, wantErr: require.Error,
}, },
{ {
name: "to dir", name: "to dir",
cfg: FileConfig{ cfg: Config{
Path: "./test-fixtures/image-simple", Path: "./test-fixtures/image-simple",
}, },
wantErr: require.Error, wantErr: require.Error,
}, },
{ {
name: "with path", name: "with path",
cfg: FileConfig{Path: "./test-fixtures/image-simple/Dockerfile"}, cfg: Config{Path: "./test-fixtures/image-simple/Dockerfile"},
want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"), want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f", wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
}, },
{ {
name: "with path and alias", name: "with path and alias",
cfg: FileConfig{ cfg: Config{
Path: "./test-fixtures/image-simple/Dockerfile", Path: "./test-fixtures/image-simple/Dockerfile",
Alias: Alias{ Alias: source.Alias{
Name: "name-me-that!", Name: "name-me-that!",
Version: "version-me-this!", Version: "version-me-this!",
}, },
@ -272,9 +280,9 @@ func Test_FileSource_ID(t *testing.T) {
}, },
{ {
name: "other fields do not affect ID", name: "other fields do not affect ID",
cfg: FileConfig{ cfg: Config{
Path: "test-fixtures/image-simple/Dockerfile", Path: "test-fixtures/image-simple/Dockerfile",
Exclude: ExcludeConfig{ Exclude: source.ExcludeConfig{
Paths: []string{"a", "b"}, Paths: []string{"a", "b"},
}, },
}, },
@ -287,11 +295,12 @@ func Test_FileSource_ID(t *testing.T) {
if tt.wantErr == nil { if tt.wantErr == nil {
tt.wantErr = require.NoError tt.wantErr = require.NoError
} }
s, err := NewFromFile(tt.cfg) newSource, err := New(tt.cfg)
tt.wantErr(t, err) tt.wantErr(t, err)
if err != nil { if err != nil {
return return
} }
s := newSource.(*fileSource)
assert.Equalf(t, tt.want, s.ID(), "ID() mismatch") assert.Equalf(t, tt.want, s.ID(), "ID() mismatch")
assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch") assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch")
}) })

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"`
}

View file

@ -1,4 +1,4 @@
package source package internal
import ( import (
"strings" "strings"
@ -6,6 +6,6 @@ import (
"github.com/anchore/syft/syft/artifact" "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:")) return artifact.ID(strings.TrimPrefix(input, "sha256:"))
} }

11
syft/source/provider.go Normal file
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)
}

View file

@ -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,
},
}
}

View file

@ -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...)...)
}

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
}

View file

@ -1,90 +1,53 @@
package source package stereoscopesource
import ( import (
"context"
"fmt" "fmt"
"github.com/bmatcuk/doublestar/v4" "github.com/bmatcuk/doublestar/v4"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver" "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 Reference string
From image.Source
Platform *image.Platform Platform *image.Platform
RegistryOptions *image.RegistryOptions RegistryOptions *image.RegistryOptions
Exclude ExcludeConfig Exclude source.ExcludeConfig
Alias Alias Alias source.Alias
} }
type StereoscopeImageSource struct { type stereoscopeImageSource struct {
id artifact.ID id artifact.ID
config StereoscopeImageConfig config ImageConfig
image *image.Image image *image.Image
metadata StereoscopeImageSourceMetadata metadata source.ImageMetadata
} }
func NewFromStereoscopeImageObject(img *image.Image, reference string, alias *Alias) (*StereoscopeImageSource, error) { func New(img *image.Image, cfg ImageConfig) source.Source {
var aliasVal Alias
if !alias.IsEmpty() {
aliasVal = *alias
}
cfg := StereoscopeImageConfig{
Reference: reference,
Alias: aliasVal,
}
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference) metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
return &stereoscopeImageSource{
return &StereoscopeImageSource{
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata), id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
config: cfg, config: cfg,
image: img, image: img,
metadata: metadata, metadata: metadata,
}, nil }
} }
func NewFromStereoscopeImage(cfg StereoscopeImageConfig) (*StereoscopeImageSource, error) { func (s stereoscopeImageSource) ID() artifact.ID {
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 {
return s.id return s.id
} }
func (s StereoscopeImageSource) Describe() Description { func (s stereoscopeImageSource) Describe() source.Description {
a := s.config.Alias a := s.config.Alias
name := a.Name name := a.Name
@ -123,7 +86,7 @@ func (s StereoscopeImageSource) Describe() Description {
nameIfUnset(s.metadata.UserInput) nameIfUnset(s.metadata.UserInput)
versionIfUnset(s.metadata.ManifestDigest) versionIfUnset(s.metadata.ManifestDigest)
return Description{ return source.Description{
ID: string(s.id), ID: string(s.id),
Name: name, Name: name,
Version: version, 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 res file.Resolver
var err error var err error
switch scope { switch scope {
case SquashedScope: case source.SquashedScope:
res, err = fileresolver.NewFromContainerImageSquash(s.image) res, err = fileresolver.NewFromContainerImageSquash(s.image)
case AllLayersScope: case source.AllLayersScope:
res, err = fileresolver.NewFromContainerImageAllLayers(s.image) res, err = fileresolver.NewFromContainerImageAllLayers(s.image)
default: default:
return nil, fmt.Errorf("bad image scope provided: %+v", scope) 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 return res, nil
} }
func (s StereoscopeImageSource) Close() error { func (s stereoscopeImageSource) Close() error {
if s.image == nil { if s.image == nil {
return nil return nil
} }
return s.image.Cleanup() 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)) tags := make([]string, len(img.Metadata.Tags))
for idx, tag := range img.Metadata.Tags { for idx, tag := range img.Metadata.Tags {
tags[idx] = tag.String() tags[idx] = tag.String()
} }
layers := make([]StereoscopeLayerMetadata, len(img.Layers)) layers := make([]source.LayerMetadata, len(img.Layers))
for idx, l := range img.Layers { for idx, l := range img.Layers {
layers[idx] = StereoscopeLayerMetadata{ layers[idx] = source.LayerMetadata{
MediaType: string(l.Metadata.MediaType), MediaType: string(l.Metadata.MediaType),
Digest: l.Metadata.Digest, Digest: l.Metadata.Digest,
Size: l.Metadata.Size, Size: l.Metadata.Size,
} }
} }
return StereoscopeImageSourceMetadata{ return source.ImageMetadata{
ID: img.Metadata.ID, ID: img.Metadata.ID,
UserInput: reference, UserInput: reference,
ManifestDigest: img.Metadata.ManifestDigest, 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 // 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. // 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 var input string
if len(metadata.RawManifest) > 0 { if len(metadata.RawManifest) > 0 {
@ -226,10 +189,10 @@ func deriveIDFromStereoscopeImage(alias Alias, metadata StereoscopeImageSourceMe
input = digest.Canonical.FromString(input + aliasStr).String() 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 { if len(lm) < 1 {
return "" return ""
} }
@ -242,7 +205,7 @@ func calculateChainID(lm []StereoscopeLayerMetadata) string {
return id return id
} }
func chain(chainID string, layers []StereoscopeLayerMetadata) string { func chain(chainID string, layers []source.LayerMetadata) string {
if len(layers) < 1 { if len(layers) < 1 {
return chainID return chainID
} }

View file

@ -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
}

View file

@ -1,6 +1,7 @@
package source package stereoscopesource
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"strings" "strings"
@ -9,12 +10,16 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/artifact" "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) { func Test_StereoscopeImage_Exclusions(t *testing.T) {
testutil.Chdir(t, "..") // run with source/test-fixtures
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
@ -76,22 +81,27 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
src, err := NewFromStereoscopeImage( imageName := strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1]
StereoscopeImageConfig{
Reference: strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1], img, err := stereoscope.GetImage(context.TODO(), imageName)
From: image.DockerTarballSource, require.NoError(t, err)
Exclude: ExcludeConfig{ require.NotNil(t, img)
src := New(
img,
ImageConfig{
Reference: imageName,
Exclude: source.ExcludeConfig{
Paths: test.exclusions, Paths: test.exclusions,
}, },
}, },
) )
require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
require.NoError(t, src.Close()) require.NoError(t, src.Close())
}) })
res, err := src.FileResolver(SquashedScope) res, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err) require.NoError(t, err)
contents, err := res.FilesByGlob(test.glob) contents, err := res.FilesByGlob(test.glob)
@ -105,15 +115,15 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
func Test_StereoscopeImageSource_ID(t *testing.T) { func Test_StereoscopeImageSource_ID(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
alias Alias alias source.Alias
metadata StereoscopeImageSourceMetadata metadata source.ImageMetadata
want artifact.ID want artifact.ID
}{ }{
{ {
name: "use raw manifest over chain ID or user input", name: "use raw manifest over chain ID or user input",
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{ Layers: []source.LayerMetadata{
{ {
Digest: "a", Digest: "a",
}, },
@ -134,9 +144,9 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
}, },
{ {
name: "use chain ID over user input", name: "use chain ID over user input",
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
//UserInput: "user-input", //UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{ Layers: []source.LayerMetadata{
{ {
Digest: "a", Digest: "a",
}, },
@ -149,7 +159,7 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
}, },
}, },
want: func() artifact.ID { want: func() artifact.ID {
metadata := []StereoscopeLayerMetadata{ metadata := []source.LayerMetadata{
{ {
Digest: "a", Digest: "a",
}, },
@ -165,7 +175,7 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
}, },
{ {
name: "use user input last", name: "use user input last",
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
}, },
want: func() artifact.ID { want: func() artifact.ID {
@ -176,9 +186,9 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
}, },
{ {
name: "without alias (first)", name: "without alias (first)",
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{ Layers: []source.LayerMetadata{
{ {
Digest: "a", Digest: "a",
}, },
@ -195,13 +205,13 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
}, },
{ {
name: "always consider alias (first)", name: "always consider alias (first)",
alias: Alias{ alias: source.Alias{
Name: "alias", Name: "alias",
Version: "version", Version: "version",
}, },
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
Layers: []StereoscopeLayerMetadata{ Layers: []source.LayerMetadata{
{ {
Digest: "a", Digest: "a",
}, },
@ -218,18 +228,18 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
}, },
{ {
name: "without alias (last)", name: "without alias (last)",
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
}, },
want: "ab0dff627d80b9753193d7280bec8f45e8ec6b4cb0912c6fffcf7cd782d9739e", want: "ab0dff627d80b9753193d7280bec8f45e8ec6b4cb0912c6fffcf7cd782d9739e",
}, },
{ {
name: "always consider alias (last)", name: "always consider alias (last)",
alias: Alias{ alias: source.Alias{
Name: "alias", Name: "alias",
Version: "version", Version: "version",
}, },
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
UserInput: "user-input", UserInput: "user-input",
}, },
want: "fe86c0eecd5654d3c0c0b2176aa394aef6440347c241aa8d9b628dfdde4287cf", want: "fe86c0eecd5654d3c0c0b2176aa394aef6440347c241aa8d9b628dfdde4287cf",
@ -245,18 +255,18 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
func Test_Describe(t *testing.T) { func Test_Describe(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
source StereoscopeImageSource source stereoscopeImageSource
expected Description expected source.Description
}{ }{
{ {
name: "name from user input", name: "name from user input",
source: StereoscopeImageSource{ source: stereoscopeImageSource{
id: "some-id", id: "some-id",
metadata: StereoscopeImageSourceMetadata{ metadata: source.ImageMetadata{
UserInput: "user input", UserInput: "user input",
}, },
}, },
expected: Description{ expected: source.Description{
ID: "some-id", ID: "some-id",
Name: "user input", Name: "user input",
}, },

View file

@ -351,7 +351,7 @@ func TestPackagesCmdFlags(t *testing.T) {
func TestRegistryAuth(t *testing.T) { func TestRegistryAuth(t *testing.T) {
host := "localhost:17" host := "localhost:17"
image := fmt.Sprintf("%s/something:latest", host) image := fmt.Sprintf("%s/something:latest", host)
args := []string{"scan", "-vvv", fmt.Sprintf("registry:%s", image)} args := []string{"scan", "-vvv", image, "--from", "registry"}
tests := []struct { tests := []struct {
name string name string
@ -363,7 +363,7 @@ func TestRegistryAuth(t *testing.T) {
name: "fallback to keychain", name: "fallback to keychain",
args: args, args: args,
assertions: []traitAssertion{ assertions: []traitAssertion{
assertInOutput("source=OciRegistry"), assertInOutput("from registry"),
assertInOutput(image), assertInOutput(image),
assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)), assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)),
}, },
@ -377,7 +377,7 @@ func TestRegistryAuth(t *testing.T) {
"SYFT_REGISTRY_AUTH_PASSWORD": "password", "SYFT_REGISTRY_AUTH_PASSWORD": "password",
}, },
assertions: []traitAssertion{ assertions: []traitAssertion{
assertInOutput("source=OciRegistry"), assertInOutput("from registry"),
assertInOutput(image), assertInOutput(image),
assertInOutput(fmt.Sprintf(`using basic auth for registry "%s"`, host)), assertInOutput(fmt.Sprintf(`using basic auth for registry "%s"`, host)),
}, },
@ -390,7 +390,7 @@ func TestRegistryAuth(t *testing.T) {
"SYFT_REGISTRY_AUTH_TOKEN": "my-token", "SYFT_REGISTRY_AUTH_TOKEN": "my-token",
}, },
assertions: []traitAssertion{ assertions: []traitAssertion{
assertInOutput("source=OciRegistry"), assertInOutput("from registry"),
assertInOutput(image), assertInOutput(image),
assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)), assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)),
}, },
@ -402,7 +402,7 @@ func TestRegistryAuth(t *testing.T) {
"SYFT_REGISTRY_AUTH_AUTHORITY": host, "SYFT_REGISTRY_AUTH_AUTHORITY": host,
}, },
assertions: []traitAssertion{ assertions: []traitAssertion{
assertInOutput("source=OciRegistry"), assertInOutput("from registry"),
assertInOutput(image), assertInOutput(image),
assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)), assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)),
}, },