From a978966cadfca10d0267aa994be766b403e6f523 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Tue, 27 Feb 2024 16:44:37 -0500 Subject: [PATCH] feat: add `--from` flag, refactor source providers (#2610) Signed-off-by: Keith Zantow --- README.md | 33 +- cmd/syft/internal/commands/attest.go | 15 +- cmd/syft/internal/commands/scan.go | 79 +++-- cmd/syft/internal/options/catalog.go | 6 + cmd/syft/internal/options/source.go | 11 +- .../test/integration/catalog_packages_test.go | 8 +- .../internal/test/integration/utils_test.go | 16 +- examples/create_custom_sbom/main.go | 12 +- examples/create_simple_sbom/main.go | 12 +- examples/select_catalogers/main.go | 12 +- examples/source_detection/main.go | 29 +- examples/source_from_image/main.go | 23 +- go.mod | 4 +- go.sum | 6 +- syft/create_sbom_config.go | 4 +- syft/create_sbom_config_test.go | 12 +- .../cataloger/filedigest/cataloger_test.go | 16 +- .../cataloger/filemetadata/cataloger_test.go | 11 +- .../internal/all_regular_files_test.go | 9 +- .../cyclonedxhelpers/to_format_model.go | 8 +- .../cyclonedxhelpers/to_format_model_test.go | 2 +- .../common/spdxhelpers/to_format_model.go | 6 +- .../spdxhelpers/to_format_model_test.go | 6 +- .../common/spdxhelpers/to_syft_model.go | 12 +- .../common/spdxhelpers/to_syft_model_test.go | 14 +- syft/format/github/internal/model/model.go | 6 +- .../github/internal/model/model_test.go | 12 +- .../internal/cyclonedxutil/helpers/decoder.go | 4 +- .../spdxutil/helpers/document_name.go | 6 +- .../spdxutil/helpers/document_name_test.go | 8 +- .../spdxutil/helpers/document_namespace.go | 6 +- .../helpers/document_namespace_test.go | 6 +- .../internal/testutil/directory_input.go | 10 +- syft/format/internal/testutil/image_input.go | 8 +- syft/format/spdxtagvalue/encoder_test.go | 2 +- syft/format/syftjson/decoder_test.go | 2 +- syft/format/syftjson/encoder_test.go | 4 +- syft/format/syftjson/model/source.go | 6 +- syft/format/syftjson/model/source_test.go | 18 +- syft/format/syftjson/to_format_model.go | 2 +- syft/format/syftjson/to_format_model_test.go | 26 +- syft/format/syftjson/to_syft_model_test.go | 26 +- syft/format/text/encoder.go | 6 +- syft/get_source.go | 101 ++++++ syft/get_source_config.go | 91 +++++ syft/internal/sourcemetadata/generated.go | 2 +- syft/internal/sourcemetadata/names.go | 6 +- syft/internal/testutil/chdir.go | 26 ++ syft/linux/identify_release_test.go | 5 +- syft/pkg/cataloger/binary/cataloger_test.go | 13 +- .../internal/pkgtest/test_generic_parser.go | 9 +- syft/source/detection.go | 207 ----------- syft/source/detection_test.go | 328 ------------------ syft/source/directory_metadata.go | 6 + .../{ => directorysource}/directory_source.go | 51 ++- .../directory_source_provider.go | 65 ++++ .../directory_source_test.go | 67 ++-- .../directory_source_win_test.go | 4 +- syft/source/file_metadata.go | 9 + syft/source/{ => filesource}/file_source.go | 49 +-- .../source/filesource/file_source_provider.go | 58 ++++ .../{ => filesource}/file_source_test.go | 43 ++- syft/source/image_metadata.go | 27 ++ syft/source/{ => internal}/digest_utils.go | 4 +- syft/source/provider.go | 11 + .../sourceproviders/source_provider_config.go | 56 +++ .../sourceproviders/source_providers.go | 55 +++ syft/source/stereoscope_image_metadata.go | 64 ---- .../image_source.go} | 93 ++--- .../image_source_provider.go | 61 ++++ .../image_source_test.go} | 70 ++-- test/cli/scan_cmd_test.go | 10 +- 72 files changed, 1035 insertions(+), 1080 deletions(-) create mode 100644 syft/get_source.go create mode 100644 syft/get_source_config.go create mode 100644 syft/internal/testutil/chdir.go delete mode 100644 syft/source/detection.go delete mode 100644 syft/source/detection_test.go create mode 100644 syft/source/directory_metadata.go rename syft/source/{ => directorysource}/directory_source.go (81%) create mode 100644 syft/source/directorysource/directory_source_provider.go rename syft/source/{ => directorysource}/directory_source_test.go (89%) rename syft/source/{ => directorysource}/directory_source_win_test.go (95%) create mode 100644 syft/source/file_metadata.go rename syft/source/{ => filesource}/file_source.go (88%) create mode 100644 syft/source/filesource/file_source_provider.go rename syft/source/{ => filesource}/file_source_test.go (89%) create mode 100644 syft/source/image_metadata.go rename syft/source/{ => internal}/digest_utils.go (63%) create mode 100644 syft/source/provider.go create mode 100644 syft/source/sourceproviders/source_provider_config.go create mode 100644 syft/source/sourceproviders/source_providers.go delete mode 100644 syft/source/stereoscope_image_metadata.go rename syft/source/{stereoscope_image_source.go => stereoscopesource/image_source.go} (71%) create mode 100644 syft/source/stereoscopesource/image_source_provider.go rename syft/source/{stereoscope_image_source_test.go => stereoscopesource/image_source_test.go} (79%) diff --git a/README.md b/README.md index 96ad78ac9..94b5f3f55 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,8 @@ syft --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 diff --git a/cmd/syft/internal/commands/attest.go b/cmd/syft/internal/commands/attest.go index e349a1cff..8e01cbc68 100644 --- a/cmd/syft/internal/commands/attest.go +++ b/cmd/syft/internal/commands/attest.go @@ -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 diff --git a/cmd/syft/internal/commands/scan.go b/cmd/syft/internal/commands/scan.go index c70d84e99..a02f6d238 100644 --- a/cmd/syft/internal/commands/scan.go +++ b/cmd/syft/internal/commands/scan.go @@ -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() +} diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index f12ea41cc..290a1d28e 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -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) diff --git a/cmd/syft/internal/options/source.go b/cmd/syft/internal/options/source.go index f582043e3..3766ce592 100644 --- a/cmd/syft/internal/options/source.go +++ b/cmd/syft/internal/options/source.go @@ -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, }, } } diff --git a/cmd/syft/internal/test/integration/catalog_packages_test.go b/cmd/syft/internal/test/integration/catalog_packages_test.go index f67adf7b7..4ba5e3b52 100644 --- a/cmd/syft/internal/test/integration/catalog_packages_test.go +++ b/cmd/syft/internal/test/integration/catalog_packages_test.go @@ -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 diff --git a/cmd/syft/internal/test/integration/utils_test.go b/cmd/syft/internal/test/integration/utils_test.go index cfca47834..88ce93d3b 100644 --- a/cmd/syft/internal/test/integration/utils_test.go +++ b/cmd/syft/internal/test/integration/utils_test.go @@ -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 diff --git a/examples/create_custom_sbom/main.go b/examples/create_custom_sbom/main.go index d55821f7e..aa51ffc0f 100644 --- a/examples/create_custom_sbom/main.go +++ b/examples/create_custom_sbom/main.go @@ -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) diff --git a/examples/create_simple_sbom/main.go b/examples/create_simple_sbom/main.go index 36a8b8597..9116f0561 100644 --- a/examples/create_simple_sbom/main.go +++ b/examples/create_simple_sbom/main.go @@ -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) diff --git a/examples/select_catalogers/main.go b/examples/select_catalogers/main.go index bc8dbefe3..79cdd4400 100644 --- a/examples/select_catalogers/main.go +++ b/examples/select_catalogers/main.go @@ -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) diff --git a/examples/source_detection/main.go b/examples/source_detection/main.go index 70fabbd04..99b00a191 100644 --- a/examples/source_detection/main.go +++ b/examples/source_detection/main.go @@ -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() +} diff --git a/examples/source_from_image/main.go b/examples/source_from_image/main.go index b5f8872fe..c4d82a290 100644 --- a/examples/source_from_image/main.go +++ b/examples/source_from_image/main.go @@ -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) diff --git a/go.mod b/go.mod index ed93ef56a..c453c3526 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6f458d07f..a15edba50 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/syft/create_sbom_config.go b/syft/create_sbom_config.go index 8acfc63f6..1f7deb18d 100644 --- a/syft/create_sbom_config.go +++ b/syft/create_sbom_config.go @@ -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) diff --git a/syft/create_sbom_config_test.go b/syft/create_sbom_config_test.go index f7ee03d3d..8c74b57cd 100644 --- a/syft/create_sbom_config_test.go +++ b/syft/create_sbom_config_test.go @@ -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... }, diff --git a/syft/file/cataloger/filedigest/cataloger_test.go b/syft/file/cataloger/filedigest/cataloger_test.go index 4cf52c79a..148a18525 100644 --- a/syft/file/cataloger/filedigest/cataloger_test.go +++ b/syft/file/cataloger/filedigest/cataloger_test.go @@ -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) diff --git a/syft/file/cataloger/filemetadata/cataloger_test.go b/syft/file/cataloger/filemetadata/cataloger_test.go index 9b2d42729..fee17c0b1 100644 --- a/syft/file/cataloger/filemetadata/cataloger_test.go +++ b/syft/file/cataloger/filemetadata/cataloger_test.go @@ -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) diff --git a/syft/file/cataloger/internal/all_regular_files_test.go b/syft/file/cataloger/internal/all_regular_files_test.go index 3f188448e..4a7d4f29f 100644 --- a/syft/file/cataloger/internal/all_regular_files_test.go +++ b/syft/file/cataloger/internal/all_regular_files_test.go @@ -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) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model.go b/syft/format/common/cyclonedxhelpers/to_format_model.go index 6ad7e8102..e2f3a285f 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model.go @@ -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 } diff --git a/syft/format/common/cyclonedxhelpers/to_format_model_test.go b/syft/format/common/cyclonedxhelpers/to_format_model_test.go index 8ac465b90..de3408082 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model_test.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model_test.go @@ -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", }, diff --git a/syft/format/common/spdxhelpers/to_format_model.go b/syft/format/common/spdxhelpers/to_format_model.go index 86a9498f6..3a2061b65 100644 --- a/syft/format/common/spdxhelpers/to_format_model.go +++ b/syft/format/common/spdxhelpers/to_format_model.go @@ -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 diff --git a/syft/format/common/spdxhelpers/to_format_model_test.go b/syft/format/common/spdxhelpers/to_format_model_test.go index 9ec557816..c2f181f0d 100644 --- a/syft/format/common/spdxhelpers/to_format_model_test.go +++ b/syft/format/common/spdxhelpers/to_format_model_test.go @@ -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{ { diff --git a/syft/format/common/spdxhelpers/to_syft_model.go b/syft/format/common/spdxhelpers/to_syft_model.go index fe045887d..34da5f726 100644 --- a/syft/format/common/spdxhelpers/to_syft_model.go +++ b/syft/format/common/spdxhelpers/to_syft_model.go @@ -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{}, } } } diff --git a/syft/format/common/spdxhelpers/to_syft_model_test.go b/syft/format/common/spdxhelpers/to_syft_model_test.go index 4b54d2f61..3f6ea6040 100644 --- a/syft/format/common/spdxhelpers/to_syft_model_test.go +++ b/syft/format/common/spdxhelpers/to_syft_model_test.go @@ -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{ { diff --git a/syft/format/github/internal/model/model.go b/syft/format/github/internal/model/model.go index d87038c6b..305407e34 100644 --- a/syft/format/github/internal/model/model.go +++ b/syft/format/github/internal/model/model.go @@ -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) diff --git a/syft/format/github/internal/model/model_test.go b/syft/format/github/internal/model/model_test.go index df97f12f5..f6fe6701d 100644 --- a/syft/format/github/internal/model/model_test.go +++ b/syft/format/github/internal/model/model_test.go @@ -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", }, } diff --git a/syft/format/internal/cyclonedxutil/helpers/decoder.go b/syft/format/internal/cyclonedxutil/helpers/decoder.go index e8a84aa63..fdf8ffe98 100644 --- a/syft/format/internal/cyclonedxutil/helpers/decoder.go +++ b/syft/format/internal/cyclonedxutil/helpers/decoder.go @@ -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{} diff --git a/syft/format/internal/spdxutil/helpers/document_name.go b/syft/format/internal/spdxutil/helpers/document_name.go index af36cc363..5f718765e 100644 --- a/syft/format/internal/spdxutil/helpers/document_name.go +++ b/syft/format/internal/spdxutil/helpers/document_name.go @@ -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" diff --git a/syft/format/internal/spdxutil/helpers/document_name_test.go b/syft/format/internal/spdxutil/helpers/document_name_test.go index 3a6f94b1f..1e1c9e72e 100644 --- a/syft/format/internal/spdxutil/helpers/document_name_test.go +++ b/syft/format/internal/spdxutil/helpers/document_name_test.go @@ -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", }, diff --git a/syft/format/internal/spdxutil/helpers/document_namespace.go b/syft/format/internal/spdxutil/helpers/document_namespace.go index 7bd6ad044..766b22071 100644 --- a/syft/format/internal/spdxutil/helpers/document_namespace.go +++ b/syft/format/internal/spdxutil/helpers/document_namespace.go @@ -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 } diff --git a/syft/format/internal/spdxutil/helpers/document_namespace_test.go b/syft/format/internal/spdxutil/helpers/document_namespace_test.go index 506252b71..1c40b2769 100644 --- a/syft/format/internal/spdxutil/helpers/document_namespace_test.go +++ b/syft/format/internal/spdxutil/helpers/document_namespace_test.go @@ -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", }, }, diff --git a/syft/format/internal/testutil/directory_input.go b/syft/format/internal/testutil/directory_input.go index 38279c3d4..e3ac03077 100644 --- a/syft/format/internal/testutil/directory_input.go +++ b/syft/format/internal/testutil/directory_input.go @@ -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, }, diff --git a/syft/format/internal/testutil/image_input.go b/syft/format/internal/testutil/image_input.go index a7757f7d7..d033b3cc5 100644 --- a/syft/format/internal/testutil/image_input.go +++ b/syft/format/internal/testutil/image_input.go @@ -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{ diff --git a/syft/format/spdxtagvalue/encoder_test.go b/syft/format/spdxtagvalue/encoder_test.go index 7dc971a8b..e5d470ec8 100644 --- a/syft/format/spdxtagvalue/encoder_test.go +++ b/syft/format/spdxtagvalue/encoder_test.go @@ -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", diff --git a/syft/format/syftjson/decoder_test.go b/syft/format/syftjson/decoder_test.go index d15aacbfa..ec403b372 100644 --- a/syft/format/syftjson/decoder_test.go +++ b/syft/format/syftjson/decoder_test.go @@ -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{ { diff --git a/syft/format/syftjson/encoder_test.go b/syft/format/syftjson/encoder_test.go index 58f356c9f..292fd6169 100644 --- a/syft/format/syftjson/encoder_test.go +++ b/syft/format/syftjson/encoder_test.go @@ -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", diff --git a/syft/format/syftjson/model/source.go b/syft/format/syftjson/model/source.go index b0e843ef2..11cf53bd1 100644 --- a/syft/format/syftjson/model/source.go +++ b/syft/format/syftjson/model/source.go @@ -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 } diff --git a/syft/format/syftjson/model/source_test.go b/syft/format/syftjson/model/source_test.go index e9118f090..c462debe7 100644 --- a/syft/format/syftjson/model/source_test.go +++ b/syft/format/syftjson/model/source_test.go @@ -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", }, }, diff --git a/syft/format/syftjson/to_format_model.go b/syft/format/syftjson/to_format_model.go index 40b1760e7..42ec48f77 100644 --- a/syft/format/syftjson/to_format_model.go +++ b/syft/format/syftjson/to_format_model.go @@ -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{} diff --git a/syft/format/syftjson/to_format_model_test.go b/syft/format/syftjson/to_format_model_test.go index 47e227dc4..a04c054ee 100644 --- a/syft/format/syftjson/to_format_model_test.go +++ b/syft/format/syftjson/to_format_model_test.go @@ -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...", diff --git a/syft/format/syftjson/to_syft_model_test.go b/syft/format/syftjson/to_syft_model_test.go index 8617bcb22..c8ef3b6b4 100644 --- a/syft/format/syftjson/to_syft_model_test.go +++ b/syft/format/syftjson/to_syft_model_test.go @@ -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{ { diff --git a/syft/format/text/encoder.go b/syft/format/text/encoder.go index acf98ffcf..b6cac6849 100644 --- a/syft/format/text/encoder.go +++ b/syft/format/text/encoder.go @@ -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 { diff --git a/syft/get_source.go b/syft/get_source.go new file mode 100644 index 000000000..c555ea70f --- /dev/null +++ b/syft/get_source.go @@ -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 +} diff --git a/syft/get_source_config.go b/syft/get_source_config.go new file mode 100644 index 000000000..7a163f1d1 --- /dev/null +++ b/syft/get_source_config.go @@ -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(), + } +} diff --git a/syft/internal/sourcemetadata/generated.go b/syft/internal/sourcemetadata/generated.go index c829f7eac..ee622ef7f 100644 --- a/syft/internal/sourcemetadata/generated.go +++ b/syft/internal/sourcemetadata/generated.go @@ -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{}} } diff --git a/syft/internal/sourcemetadata/names.go b/syft/internal/sourcemetadata/names.go index 391a9c2d9..58e56fe8f 100644 --- a/syft/internal/sourcemetadata/names.go +++ b/syft/internal/sourcemetadata/names.go @@ -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 { diff --git a/syft/internal/testutil/chdir.go b/syft/internal/testutil/chdir.go new file mode 100644 index 000000000..7c47bdb01 --- /dev/null +++ b/syft/internal/testutil/chdir.go @@ -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)) + }) +} diff --git a/syft/linux/identify_release_test.go b/syft/linux/identify_release_test.go index 9af79fcdf..724f038df 100644 --- a/syft/linux/identify_release_test.go +++ b/syft/linux/identify_release_test.go @@ -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) diff --git a/syft/pkg/cataloger/binary/cataloger_test.go b/syft/pkg/cataloger/binary/cataloger_test.go index 2fd6024bc..b390bde5e 100644 --- a/syft/pkg/cataloger/binary/cataloger_test.go +++ b/syft/pkg/cataloger/binary/cataloger_test.go @@ -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) diff --git a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go index ee07d65ea..11efb263b 100644 --- a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go +++ b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go @@ -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) diff --git a/syft/source/detection.go b/syft/source/detection.go deleted file mode 100644 index 4515c43a3..000000000 --- a/syft/source/detection.go +++ /dev/null @@ -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 - } -} diff --git a/syft/source/detection_test.go b/syft/source/detection_test.go deleted file mode 100644 index dd8a0276e..000000000 --- a/syft/source/detection_test.go +++ /dev/null @@ -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") - }) - } -} diff --git a/syft/source/directory_metadata.go b/syft/source/directory_metadata.go new file mode 100644 index 000000000..9f2d4900a --- /dev/null +++ b/syft/source/directory_metadata.go @@ -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) +} diff --git a/syft/source/directory_source.go b/syft/source/directorysource/directory_source.go similarity index 81% rename from syft/source/directory_source.go rename to syft/source/directorysource/directory_source.go index 3b974521c..2a4ab3705 100644 --- a/syft/source/directory_source.go +++ b/syft/source/directorysource/directory_source.go @@ -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 } diff --git a/syft/source/directorysource/directory_source_provider.go b/syft/source/directorysource/directory_source_provider.go new file mode 100644 index 000000000..11eed73d8 --- /dev/null +++ b/syft/source/directorysource/directory_source_provider.go @@ -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 +} diff --git a/syft/source/directory_source_test.go b/syft/source/directorysource/directory_source_test.go similarity index 89% rename from syft/source/directory_source_test.go rename to syft/source/directorysource/directory_source_test.go index 3ea197e29..751d47662 100644 --- a/syft/source/directory_source_test.go +++ b/syft/source/directorysource/directory_source_test.go @@ -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) diff --git a/syft/source/directory_source_win_test.go b/syft/source/directorysource/directory_source_win_test.go similarity index 95% rename from syft/source/directory_source_win_test.go rename to syft/source/directorysource/directory_source_win_test.go index aa0d0a277..b0d1d2e06 100644 --- a/syft/source/directory_source_win_test.go +++ b/syft/source/directorysource/directory_source_win_test.go @@ -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 { diff --git a/syft/source/file_metadata.go b/syft/source/file_metadata.go new file mode 100644 index 000000000..19190072b --- /dev/null +++ b/syft/source/file_metadata.go @@ -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"` +} diff --git a/syft/source/file_source.go b/syft/source/filesource/file_source.go similarity index 88% rename from syft/source/file_source.go rename to syft/source/filesource/file_source.go index 9e0bd5a86..d810a95c2 100644 --- a/syft/source/file_source.go +++ b/syft/source/filesource/file_source.go @@ -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 } diff --git a/syft/source/filesource/file_source_provider.go b/syft/source/filesource/file_source_provider.go new file mode 100644 index 000000000..2cba17054 --- /dev/null +++ b/syft/source/filesource/file_source_provider.go @@ -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, + }, + ) +} diff --git a/syft/source/file_source_test.go b/syft/source/filesource/file_source_test.go similarity index 89% rename from syft/source/file_source_test.go rename to syft/source/filesource/file_source_test.go index 54475c7c1..1f046d253 100644 --- a/syft/source/file_source_test.go +++ b/syft/source/filesource/file_source_test.go @@ -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") }) diff --git a/syft/source/image_metadata.go b/syft/source/image_metadata.go new file mode 100644 index 000000000..aa4fe60dd --- /dev/null +++ b/syft/source/image_metadata.go @@ -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"` +} diff --git a/syft/source/digest_utils.go b/syft/source/internal/digest_utils.go similarity index 63% rename from syft/source/digest_utils.go rename to syft/source/internal/digest_utils.go index 6c7f2feeb..075b3bdfb 100644 --- a/syft/source/digest_utils.go +++ b/syft/source/internal/digest_utils.go @@ -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:")) } diff --git a/syft/source/provider.go b/syft/source/provider.go new file mode 100644 index 000000000..b1c860e8a --- /dev/null +++ b/syft/source/provider.go @@ -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) +} diff --git a/syft/source/sourceproviders/source_provider_config.go b/syft/source/sourceproviders/source_provider_config.go new file mode 100644 index 000000000..dbc48bc0b --- /dev/null +++ b/syft/source/sourceproviders/source_provider_config.go @@ -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, + }, + } +} diff --git a/syft/source/sourceproviders/source_providers.go b/syft/source/sourceproviders/source_providers.go new file mode 100644 index 000000000..107f3ad48 --- /dev/null +++ b/syft/source/sourceproviders/source_providers.go @@ -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...)...) +} diff --git a/syft/source/stereoscope_image_metadata.go b/syft/source/stereoscope_image_metadata.go deleted file mode 100644 index 8637995fc..000000000 --- a/syft/source/stereoscope_image_metadata.go +++ /dev/null @@ -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 -} diff --git a/syft/source/stereoscope_image_source.go b/syft/source/stereoscopesource/image_source.go similarity index 71% rename from syft/source/stereoscope_image_source.go rename to syft/source/stereoscopesource/image_source.go index 53bca0025..71afe3cd8 100644 --- a/syft/source/stereoscope_image_source.go +++ b/syft/source/stereoscopesource/image_source.go @@ -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 } diff --git a/syft/source/stereoscopesource/image_source_provider.go b/syft/source/stereoscopesource/image_source_provider.go new file mode 100644 index 000000000..a579413ac --- /dev/null +++ b/syft/source/stereoscopesource/image_source_provider.go @@ -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 +} diff --git a/syft/source/stereoscope_image_source_test.go b/syft/source/stereoscopesource/image_source_test.go similarity index 79% rename from syft/source/stereoscope_image_source_test.go rename to syft/source/stereoscopesource/image_source_test.go index 32f89ae4d..1af81986c 100644 --- a/syft/source/stereoscope_image_source_test.go +++ b/syft/source/stereoscopesource/image_source_test.go @@ -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", }, diff --git a/test/cli/scan_cmd_test.go b/test/cli/scan_cmd_test.go index df33ecef6..02cb1f921 100644 --- a/test/cli/scan_cmd_test.go +++ b/test/cli/scan_cmd_test.go @@ -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)), },