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
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
# 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
```
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
podman:yourrepo/yourimage:tag use images from the Podman daemon
containerd:yourrepo/yourimage:tag use images from the Containerd daemon
docker-archive:path/to/yourimage.tar 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-dir:path/to/yourimage 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
dir:path/to/yourproject read directly from a path on disk (any directory)
file:path/to/yourproject/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)
docker use images from the Docker daemon
podman use images from the Podman daemon
containerd use images from the Containerd daemon
docker-archive use a tarball from disk for archives created from "docker save"
oci-archive use a tarball from disk for OCI archives (from Skopeo or otherwise)
oci-dir read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
singularity read directly from a Singularity Image Format (SIF) container on disk
dir read directly from a path on disk (any directory)
file read directly from a path on disk (any single file)
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.
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).
This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](#configuration) for more details).
### File selection

View file

@ -13,6 +13,7 @@ import (
"github.com/wagoodman/go-progress"
"github.com/anchore/clio"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
@ -26,7 +27,6 @@ import (
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
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) {
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 {
return nil, err
@ -273,13 +277,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
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 {
_, err := exec.LookPath(cmd)
return err == nil

View file

@ -13,6 +13,8 @@ import (
"gopkg.in/yaml.v3"
"github.com/anchore/clio"
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
@ -24,6 +26,7 @@ import (
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/sourceproviders"
)
const (
@ -162,14 +165,23 @@ func validateArgs(cmd *cobra.Command, args []string, error string) error {
return cobra.MaximumNArgs(1)(cmd, args)
}
// nolint:funlen
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
writer, err := opts.SBOMWriter()
if err != nil {
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 {
return err
@ -199,23 +211,21 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
return nil
}
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: opts.Source.Image.DefaultPullSource,
},
)
if err != nil {
return nil, fmt.Errorf("could not deteremine source: %w", err)
}
for _, filter := range filters {
if err := filter(detection); err != nil {
return nil, err
}
}
func getSource(ctx context.Context, opts *options.Catalog, userInput string, sources ...string) (source.Source, error) {
cfg := syft.DefaultGetSourceConfig().
WithRegistryOptions(opts.Registry.ToOptions()).
WithAlias(source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
}).
WithExcludeConfig(source.ExcludeConfig{
Paths: opts.Exclusions,
}).
WithBasePath(opts.Source.BasePath).
WithSources(sources...).
WithDefaultImagePullSource(opts.Source.Image.DefaultPullSource)
var err error
var platform *image.Platform
if opts.Platform != "" {
@ -223,29 +233,22 @@ func getSource(opts *options.Catalog, userInput string, filters ...func(*source.
if err != nil {
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 {
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 userInput == "power-user" {
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 {
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)
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
From []string `yaml:"from" json:"from" mapstructure:"from"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
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",
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", "",
"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
}
cfg.From = flatten(cfg.From)
cfg.Catalogers = flatten(cfg.Catalogers)
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)

View file

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

View file

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

View file

@ -34,17 +34,13 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
// get the fixture image tar file
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
userInput := "docker-archive:" + tarPath
// get the source to build an SBOM against
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
require.NoError(t, err)
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
t.Cleanup(func() {
theSource.Close()
require.NoError(t, theSource.Close())
})
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)
// get the source to build an sbom against
userInput := "dir:" + dir
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
require.NoError(t, err)
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
require.NoError(t, err)
t.Cleanup(func() {
theSource.Close()
require.NoError(t, theSource.Close())
})
// build the SBOM

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,13 @@
package main
import (
"context"
"encoding/json"
"os"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/source"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image/oci"
"github.com/anchore/syft/syft/source/stereoscopesource"
)
/*
@ -16,22 +18,15 @@ import (
const defaultImage = "alpine:3.19"
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 {
panic(err)
}
src, err := source.NewFromStereoscopeImage(
source.StereoscopeImageConfig{
Reference: imageReference(),
From: image.OciRegistrySource, // always use the registry, there are several other "Source" options here
Platform: platform,
},
)
if err != nil {
panic(err)
}
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: imageReference(),
})
// Show a basic description of the source to the screen
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-version v1.2.2-0.20200701162849-18adb9c92b9b
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
// 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
@ -80,6 +80,8 @@ require (
modernc.org/sqlite v1.29.2
)
require github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537
require (
dario.cat/mergo v1.0.0 // 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/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/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/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
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/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/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5 h1:o//fhRcSpOYHC/xG/HiI6ddtSMiRgHlB96xJQXZawZM=
github.com/anchore/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5/go.mod h1:o0TqYkefad6kIPtmbigFKss7P48z4bjd8Vp5Wklbf3Y=
github.com/anchore/stereoscope v0.0.2-0.20240221144950-cf0e754f5b56 h1:iHvTXZA+qEozPGRRuW1Mv7r7w2fHeJdzWDx+YsSIbyg=
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/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
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) {
switch m := src.Metadata.(type) {
case source.StereoscopeImageSourceMetadata:
case source.ImageMetadata:
return pkgcataloging.ImageTag, nil
case source.FileSourceMetadata, source.DirectorySourceMetadata:
case source.FileMetadata, source.DirectoryMetadata:
return pkgcataloging.DirectoryTag, nil
default:
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{
Metadata: source.StereoscopeImageSourceMetadata{},
Metadata: source.ImageMetadata{},
}
dirSrc := source.Description{
Metadata: source.DirectorySourceMetadata{},
Metadata: source.DirectoryMetadata{},
}
fileSrc := source.Description{
Metadata: source.FileSourceMetadata{},
Metadata: source.FileMetadata{},
}
tests := []struct {
@ -437,21 +437,21 @@ func Test_findDefaultTag(t *testing.T) {
{
name: "image",
src: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{},
Metadata: source.ImageMetadata{},
},
want: pkgcataloging.ImageTag,
},
{
name: "directory",
src: source.Description{
Metadata: source.DirectorySourceMetadata{},
Metadata: source.DirectoryMetadata{},
},
want: pkgcataloging.DirectoryTag,
},
{
name: "file",
src: source.Description{
Metadata: source.FileSourceMetadata{},
Metadata: source.FileMetadata{},
},
want: pkgcataloging.DirectoryTag, // not a mistake...
},

View file

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

View file

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

View file

@ -12,6 +12,8 @@ import (
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/file"
"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) {
@ -28,8 +30,9 @@ func Test_allRegularFiles(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
s, err := source.NewFromStereoscopeImageObject(img, testImage, nil)
require.NoError(t, err)
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: testImage,
})
r, err := s.FileResolver(source.SquashedScope)
require.NoError(t, err)
@ -42,7 +45,7 @@ func Test_allRegularFiles(t *testing.T) {
{
name: "directory",
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)
r, err := s.FileResolver(source.SquashedScope)
require.NoError(t, err)

View file

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

View file

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

View file

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

View file

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

View file

@ -146,7 +146,7 @@ func containerSource(p *spdx.Package) source.Description {
ID: id,
Name: p.PackageName,
Version: p.PackageVersion,
Metadata: source.StereoscopeImageSourceMetadata{
Metadata: source.ImageMetadata{
UserInput: container,
ID: id,
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) {
version := p.PackageVersion
m := source.FileSourceMetadata{
m := source.FileMetadata{
Path: p.PackageName,
}
// 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) {
return source.DirectorySourceMetadata{
return source.DirectoryMetadata{
Path: p.PackageName,
Base: "",
}, p.PackageVersion
@ -229,15 +229,15 @@ func extractSourceFromNamespace(ns string) source.Description {
switch p {
case helpers.InputFile:
return source.Description{
Metadata: source.FileSourceMetadata{},
Metadata: source.FileMetadata{},
}
case helpers.InputImage:
return source.Description{
Metadata: source.StereoscopeImageSourceMetadata{},
Metadata: source.ImageMetadata{},
}
case helpers.InputDirectory:
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",
expected: source.FileSourceMetadata{},
expected: source.FileMetadata{},
},
{
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",
expected: source.DirectorySourceMetadata{},
expected: source.DirectoryMetadata{},
},
{
namespace: "https://another-host/blob/123",
@ -460,7 +460,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
name: "image source",
source: source.Description{
ID: "DocumentRoot-Image-some-image",
Metadata: source.StereoscopeImageSourceMetadata{
Metadata: source.ImageMetadata{
ID: "DocumentRoot-Image-some-image",
UserInput: "some-image:some-tag",
ManifestDigest: "sha256:ab8b83234bc28f28d8e",
@ -476,7 +476,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
source: source.Description{
ID: "DocumentRoot-Directory-.",
Name: ".",
Metadata: source.DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: ".",
},
},
@ -488,7 +488,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
source: source.Description{
ID: "DocumentRoot-Directory-my-app",
Name: "my-app",
Metadata: source.DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: "my-app",
},
},
@ -499,7 +499,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
name: "file source",
source: source.Description{
ID: "DocumentRoot-File-my-app.exe",
Metadata: source.FileSourceMetadata{
Metadata: source.FileMetadata{
Path: "my-app.exe",
Digests: []file.Digest{
{

View file

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

View file

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

View file

@ -222,7 +222,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
ID: "",
// 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,
ID: c.BOMRef,
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
return source.Description{
ID: "",
Metadata: source.FileSourceMetadata{Path: c.Name},
Metadata: source.FileMetadata{Path: c.Name},
}
}
return source.Description{}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/filetree"
@ -16,7 +15,7 @@ import (
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"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 {
@ -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
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
src, err := source.NewFromStereoscopeImageObject(img, "user-image-input", nil)
assert.NoError(t, err)
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: "user-image-input",
})
return sbom.SBOM{
Artifacts: sbom.Artifacts{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -303,7 +303,7 @@ func toSourceModel(src source.Description) model.Source {
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
if metadata.RepoDigests == nil {
metadata.RepoDigests = []string{}

View file

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

View file

@ -35,7 +35,7 @@ func Test_toSyftSourceData(t *testing.T) {
Name: "some-name",
Version: "some-version",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: "some/path",
Base: "some/base",
},
@ -44,7 +44,7 @@ func Test_toSyftSourceData(t *testing.T) {
ID: "the-id",
Name: "some-name",
Version: "some-version",
Metadata: source.DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: "some/path",
Base: "some/base",
},
@ -57,7 +57,7 @@ func Test_toSyftSourceData(t *testing.T) {
Name: "some-name",
Version: "some-version",
Type: "file",
Metadata: source.FileSourceMetadata{
Metadata: source.FileMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
@ -67,7 +67,7 @@ func Test_toSyftSourceData(t *testing.T) {
ID: "the-id",
Name: "some-name",
Version: "some-version",
Metadata: source.FileSourceMetadata{
Metadata: source.FileMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
@ -81,7 +81,7 @@ func Test_toSyftSourceData(t *testing.T) {
Name: "some-name",
Version: "some-version",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
Metadata: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
@ -92,7 +92,7 @@ func Test_toSyftSourceData(t *testing.T) {
ID: "the-id",
Name: "some-name",
Version: "some-version",
Metadata: source.StereoscopeImageSourceMetadata{
Metadata: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
@ -107,14 +107,14 @@ func Test_toSyftSourceData(t *testing.T) {
src: model.Source{
ID: "the-id",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: "some/path",
Base: "some/base",
},
},
expected: &source.Description{
ID: "the-id",
Metadata: source.DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: "some/path",
Base: "some/base",
},
@ -125,7 +125,7 @@ func Test_toSyftSourceData(t *testing.T) {
src: model.Source{
ID: "the-id",
Type: "file",
Metadata: source.FileSourceMetadata{
Metadata: source.FileMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
@ -133,7 +133,7 @@ func Test_toSyftSourceData(t *testing.T) {
},
expected: &source.Description{
ID: "the-id",
Metadata: source.FileSourceMetadata{
Metadata: source.FileMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
@ -145,7 +145,7 @@ func Test_toSyftSourceData(t *testing.T) {
src: model.Source{
ID: "the-id",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
Metadata: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
@ -154,7 +154,7 @@ func Test_toSyftSourceData(t *testing.T) {
},
expected: &source.Description{
ID: "the-id",
Metadata: source.StereoscopeImageSourceMetadata{
Metadata: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
@ -178,7 +178,7 @@ func Test_idsHaveChanged(t *testing.T) {
s := toSyftModel(model.Document{
Source: model.Source{
Type: "file",
Metadata: source.FileSourceMetadata{Path: "some/path"},
Metadata: source.FileMetadata{Path: "some/path"},
},
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)
switch metadata := s.Source.Metadata.(type) {
case source.DirectorySourceMetadata:
case source.DirectoryMetadata:
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
case source.FileSourceMetadata:
case source.FileMetadata:
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
case source.StereoscopeImageSourceMetadata:
case source.ImageMetadata:
fmt.Fprintln(w, "[Image]")
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).
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{
reflect.TypeOf(source.DirectorySourceMetadata{}): {"directory", "dir"},
reflect.TypeOf(source.FileSourceMetadata{}): {"file"},
reflect.TypeOf(source.StereoscopeImageSourceMetadata{}): {"image"},
reflect.TypeOf(source.DirectoryMetadata{}): {"directory", "dir"},
reflect.TypeOf(source.FileMetadata{}): {"file"},
reflect.TypeOf(source.ImageMetadata{}): {"image"},
}
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/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
)
func TestIdentifyRelease(t *testing.T) {
@ -336,7 +337,9 @@ func TestIdentifyRelease(t *testing.T) {
for _, test := range tests {
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)
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/cataloger/binary/test-fixtures/manager/testutil"
"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)")
@ -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.
path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries)
src, err := source.NewFromDirectoryPath(path)
src, err := directorysource.NewFromPath(path)
require.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope)
@ -936,8 +938,9 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
src, err := source.NewFromStereoscopeImageObject(img, test.fixtureImage, nil)
require.NoError(t, err)
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: test.fixtureImage,
})
resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
@ -966,7 +969,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
src, err := source.NewFromDirectoryPath("test-fixtures/classifiers/negative")
src, err := directorysource.NewFromPath("test-fixtures/classifiers/negative")
assert.NoError(t, err)
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) {
c := NewClassifierCataloger(test.config)
src, err := source.NewFromDirectoryPath(test.fixtureDir)
src, err := directorysource.NewFromPath(test.fixtureDir)
require.NoError(t, err)
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/cataloger/generic"
"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
@ -88,7 +90,7 @@ func DefaultLicenseComparer(x, y pkg.License) bool {
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
t.Helper()
s, err := source.NewFromDirectoryPath(path)
s, err := directorysource.NewFromPath(path)
require.NoError(t, err)
resolver, err := s.FileResolver(source.AllLayersScope)
@ -152,8 +154,9 @@ func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *Cat
t.Helper()
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
s, err := source.NewFromStereoscopeImageObject(img, fixtureName, nil)
require.NoError(t, err)
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: fixtureName,
})
r, err := s.FileResolver(source.SquashedScope)
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 (
"fmt"
@ -14,37 +14,34 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/internal"
)
var _ Source = (*DirectorySource)(nil)
var _ source.Source = (*directorySource)(nil)
type DirectoryConfig struct {
type Config struct {
Path string
Base string
Exclude ExcludeConfig
Alias Alias
Exclude source.ExcludeConfig
Alias source.Alias
}
type DirectorySourceMetadata struct {
Path string `json:"path" yaml:"path"`
Base string `json:"-" yaml:"-"` // though this is important, for display purposes it leaks too much information (abs paths)
}
type DirectorySource struct {
type directorySource struct {
id artifact.ID
config DirectoryConfig
config Config
resolver *fileresolver.Directory
mutex *sync.Mutex
}
func NewFromDirectoryPath(path string) (*DirectorySource, error) {
cfg := DirectoryConfig{
func NewFromPath(path string) (source.Source, error) {
cfg := Config{
Path: path,
}
return NewFromDirectory(cfg)
return New(cfg)
}
func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
func New(cfg Config) (source.Source, error) {
fi, err := os.Stat(cfg.Path)
if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
@ -54,7 +51,7 @@ func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
}
return &DirectorySource{
return &directorySource{
id: deriveIDFromDirectory(cfg),
config: cfg,
mutex: &sync.Mutex{},
@ -66,7 +63,7 @@ func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
// consideration for the path.
func deriveIDFromDirectory(cfg DirectoryConfig) artifact.ID {
func deriveIDFromDirectory(cfg Config) artifact.ID {
var info string
if !cfg.Alias.IsEmpty() {
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
@ -78,7 +75,7 @@ func deriveIDFromDirectory(cfg DirectoryConfig) artifact.ID {
info = cleanDirPath(cfg.Path, cfg.Base)
}
return artifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
}
func cleanDirPath(path, base string) string {
@ -108,11 +105,11 @@ func cleanDirPath(path, base string) string {
return path
}
func (s DirectorySource) ID() artifact.ID {
func (s directorySource) ID() artifact.ID {
return s.id
}
func (s DirectorySource) Describe() Description {
func (s directorySource) Describe() source.Description {
name := cleanDirPath(s.config.Path, s.config.Base)
version := ""
if !s.config.Alias.IsEmpty() {
@ -124,23 +121,23 @@ func (s DirectorySource) Describe() Description {
version = a.Version
}
}
return Description{
return source.Description{
ID: string(s.id),
Name: name,
Version: version,
Metadata: DirectorySourceMetadata{
Metadata: source.DirectoryMetadata{
Path: s.config.Path,
Base: s.config.Base,
},
}
}
func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) {
func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.resolver == nil {
exclusionFunctions, err := getDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
if err != nil {
return nil, err
}
@ -156,14 +153,14 @@ func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) {
return s.resolver, nil
}
func (s *DirectorySource) Close() error {
func (s *directorySource) Close() error {
s.mutex.Lock()
defer s.mutex.Unlock()
s.resolver = nil
return nil
}
func getDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, error) {
func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, error) {
if len(exclusions) == 0 {
return nil, nil
}

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

View file

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

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 (
"crypto"
@ -18,27 +18,24 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/internal"
)
var _ Source = (*FileSource)(nil)
var _ source.Source = (*fileSource)(nil)
type FileConfig struct {
type Config struct {
Path string
Exclude ExcludeConfig
Exclude source.ExcludeConfig
DigestAlgorithms []crypto.Hash
Alias Alias
Alias source.Alias
}
type FileSourceMetadata struct {
Path string `json:"path" yaml:"path"`
Digests []file.Digest `json:"digests,omitempty" yaml:"digests,omitempty"`
MIMEType string `json:"mimeType" yaml:"mimeType"`
}
type FileSource struct {
type fileSource struct {
id artifact.ID
digestForVersion string
config FileConfig
config Config
resolver *fileresolver.Directory
mutex *sync.Mutex
closer func() error
@ -47,7 +44,11 @@ type FileSource struct {
analysisPath string
}
func NewFromFile(cfg FileConfig) (*FileSource, error) {
func NewFromPath(path string) (source.Source, error) {
return New(Config{Path: path})
}
func New(cfg Config) (source.Source, error) {
fileMeta, err := os.Stat(cfg.Path)
if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
@ -83,7 +84,7 @@ func NewFromFile(cfg FileConfig) (*FileSource, error) {
id, versionDigest := deriveIDFromFile(cfg)
return &FileSource{
return &fileSource{
id: id,
config: cfg,
mutex: &sync.Mutex{},
@ -98,7 +99,7 @@ func NewFromFile(cfg FileConfig) (*FileSource, error) {
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
// logically different, then ID will express that.
func deriveIDFromFile(cfg FileConfig) (artifact.ID, string) {
func deriveIDFromFile(cfg Config) (artifact.ID, string) {
d := digestOfFileContents(cfg.Path)
info := d
@ -108,14 +109,14 @@ func deriveIDFromFile(cfg FileConfig) (artifact.ID, string) {
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
}
return artifactIDFromDigest(digest.SHA256.FromString(info).String()), d
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d
}
func (s FileSource) ID() artifact.ID {
func (s fileSource) ID() artifact.ID {
return s.id
}
func (s FileSource) Describe() Description {
func (s fileSource) Describe() source.Description {
name := path.Base(s.config.Path)
version := s.digestForVersion
if !s.config.Alias.IsEmpty() {
@ -128,11 +129,11 @@ func (s FileSource) Describe() Description {
version = a.Version
}
}
return Description{
return source.Description{
ID: string(s.id),
Name: name,
Version: version,
Metadata: FileSourceMetadata{
Metadata: source.FileMetadata{
Path: s.config.Path,
Digests: s.digests,
MIMEType: s.mimeType,
@ -140,7 +141,7 @@ func (s FileSource) Describe() Description {
}
}
func (s FileSource) FileResolver(_ Scope) (file.Resolver, error) {
func (s fileSource) FileResolver(_ source.Scope) (file.Resolver, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
@ -148,7 +149,7 @@ func (s FileSource) FileResolver(_ Scope) (file.Resolver, error) {
return s.resolver, nil
}
exclusionFunctions, err := getDirectoryExclusionFunctions(s.analysisPath, s.config.Exclude.Paths)
exclusionFunctions, err := directorysource.GetDirectoryExclusionFunctions(s.analysisPath, s.config.Exclude.Paths)
if err != nil {
return nil, err
}
@ -221,7 +222,7 @@ func absoluteSymlinkFreePathToParent(path string) (string, error) {
return filepath.Dir(dereferencedAbsAnalysisPath), nil
}
func (s *FileSource) Close() error {
func (s *fileSource) Close() error {
if s.closer == nil {
return nil
}

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

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 (
"strings"
@ -6,6 +6,6 @@ import (
"github.com/anchore/syft/syft/artifact"
)
func artifactIDFromDigest(input string) artifact.ID {
func ArtifactIDFromDigest(input string) artifact.ID {
return artifact.ID(strings.TrimPrefix(input, "sha256:"))
}

11
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 (
"context"
"fmt"
"github.com/bmatcuk/doublestar/v4"
"github.com/distribution/reference"
"github.com/opencontainers/go-digest"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/internal"
)
var _ Source = (*StereoscopeImageSource)(nil)
var _ source.Source = (*stereoscopeImageSource)(nil)
type StereoscopeImageConfig struct {
type ImageConfig struct {
Reference string
From image.Source
Platform *image.Platform
RegistryOptions *image.RegistryOptions
Exclude ExcludeConfig
Alias Alias
Exclude source.ExcludeConfig
Alias source.Alias
}
type StereoscopeImageSource struct {
type stereoscopeImageSource struct {
id artifact.ID
config StereoscopeImageConfig
config ImageConfig
image *image.Image
metadata StereoscopeImageSourceMetadata
metadata source.ImageMetadata
}
func NewFromStereoscopeImageObject(img *image.Image, reference string, alias *Alias) (*StereoscopeImageSource, error) {
var aliasVal Alias
if !alias.IsEmpty() {
aliasVal = *alias
}
cfg := StereoscopeImageConfig{
Reference: reference,
Alias: aliasVal,
}
func New(img *image.Image, cfg ImageConfig) source.Source {
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
return &StereoscopeImageSource{
return &stereoscopeImageSource{
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
config: cfg,
image: img,
metadata: metadata,
}, nil
}
}
func NewFromStereoscopeImage(cfg StereoscopeImageConfig) (*StereoscopeImageSource, error) {
ctx := context.TODO()
var opts []stereoscope.Option
if cfg.RegistryOptions != nil {
opts = append(opts, stereoscope.WithRegistryOptions(*cfg.RegistryOptions))
}
if cfg.Platform != nil {
opts = append(opts, stereoscope.WithPlatform(cfg.Platform.String()))
}
img, err := stereoscope.GetImageFromSource(ctx, cfg.Reference, cfg.From, opts...)
if err != nil {
return nil, fmt.Errorf("unable to load image: %w", err)
}
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
return &StereoscopeImageSource{
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
config: cfg,
image: img,
metadata: metadata,
}, nil
}
func (s StereoscopeImageSource) ID() artifact.ID {
func (s stereoscopeImageSource) ID() artifact.ID {
return s.id
}
func (s StereoscopeImageSource) Describe() Description {
func (s stereoscopeImageSource) Describe() source.Description {
a := s.config.Alias
name := a.Name
@ -123,7 +86,7 @@ func (s StereoscopeImageSource) Describe() Description {
nameIfUnset(s.metadata.UserInput)
versionIfUnset(s.metadata.ManifestDigest)
return Description{
return source.Description{
ID: string(s.id),
Name: name,
Version: version,
@ -131,14 +94,14 @@ func (s StereoscopeImageSource) Describe() Description {
}
}
func (s StereoscopeImageSource) FileResolver(scope Scope) (file.Resolver, error) {
func (s stereoscopeImageSource) FileResolver(scope source.Scope) (file.Resolver, error) {
var res file.Resolver
var err error
switch scope {
case SquashedScope:
case source.SquashedScope:
res, err = fileresolver.NewFromContainerImageSquash(s.image)
case AllLayersScope:
case source.AllLayersScope:
res, err = fileresolver.NewFromContainerImageAllLayers(s.image)
default:
return nil, fmt.Errorf("bad image scope provided: %+v", scope)
@ -156,29 +119,29 @@ func (s StereoscopeImageSource) FileResolver(scope Scope) (file.Resolver, error)
return res, nil
}
func (s StereoscopeImageSource) Close() error {
func (s stereoscopeImageSource) Close() error {
if s.image == nil {
return nil
}
return s.image.Cleanup()
}
func imageMetadataFromStereoscopeImage(img *image.Image, reference string) StereoscopeImageSourceMetadata {
func imageMetadataFromStereoscopeImage(img *image.Image, reference string) source.ImageMetadata {
tags := make([]string, len(img.Metadata.Tags))
for idx, tag := range img.Metadata.Tags {
tags[idx] = tag.String()
}
layers := make([]StereoscopeLayerMetadata, len(img.Layers))
layers := make([]source.LayerMetadata, len(img.Layers))
for idx, l := range img.Layers {
layers[idx] = StereoscopeLayerMetadata{
layers[idx] = source.LayerMetadata{
MediaType: string(l.Metadata.MediaType),
Digest: l.Metadata.Digest,
Size: l.Metadata.Size,
}
}
return StereoscopeImageSourceMetadata{
return source.ImageMetadata{
ID: img.Metadata.ID,
UserInput: reference,
ManifestDigest: img.Metadata.ManifestDigest,
@ -203,7 +166,7 @@ func imageMetadataFromStereoscopeImage(img *image.Image, reference string) Stere
//
// in all cases, if an alias is provided, it is additionally considered in the ID calculation. This allows for the
// same image to be scanned multiple times with different aliases and be considered logically different.
func deriveIDFromStereoscopeImage(alias Alias, metadata StereoscopeImageSourceMetadata) artifact.ID {
func deriveIDFromStereoscopeImage(alias source.Alias, metadata source.ImageMetadata) artifact.ID {
var input string
if len(metadata.RawManifest) > 0 {
@ -226,10 +189,10 @@ func deriveIDFromStereoscopeImage(alias Alias, metadata StereoscopeImageSourceMe
input = digest.Canonical.FromString(input + aliasStr).String()
}
return artifactIDFromDigest(input)
return internal.ArtifactIDFromDigest(input)
}
func calculateChainID(lm []StereoscopeLayerMetadata) string {
func calculateChainID(lm []source.LayerMetadata) string {
if len(lm) < 1 {
return ""
}
@ -242,7 +205,7 @@ func calculateChainID(lm []StereoscopeLayerMetadata) string {
return id
}
func chain(chainID string, layers []StereoscopeLayerMetadata) string {
func chain(chainID string, layers []source.LayerMetadata) string {
if len(layers) < 1 {
return chainID
}

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

View file

@ -351,7 +351,7 @@ func TestPackagesCmdFlags(t *testing.T) {
func TestRegistryAuth(t *testing.T) {
host := "localhost:17"
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 {
name string
@ -363,7 +363,7 @@ func TestRegistryAuth(t *testing.T) {
name: "fallback to keychain",
args: args,
assertions: []traitAssertion{
assertInOutput("source=OciRegistry"),
assertInOutput("from registry"),
assertInOutput(image),
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",
},
assertions: []traitAssertion{
assertInOutput("source=OciRegistry"),
assertInOutput("from registry"),
assertInOutput(image),
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",
},
assertions: []traitAssertion{
assertInOutput("source=OciRegistry"),
assertInOutput("from registry"),
assertInOutput(image),
assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)),
},
@ -402,7 +402,7 @@ func TestRegistryAuth(t *testing.T) {
"SYFT_REGISTRY_AUTH_AUTHORITY": host,
},
assertions: []traitAssertion{
assertInOutput("source=OciRegistry"),
assertInOutput("from registry"),
assertInOutput(image),
assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)),
},