From c363b2b53257ffb3cf82f98eeaaf1482c463ee79 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 13 Apr 2021 09:30:57 -0400 Subject: [PATCH] Add ability to pull images directly from a registry (#378) * add registry image source Signed-off-by: Alex Goodman * use explicit source for fetching image + add scheme and registry tests Signed-off-by: Alex Goodman * adjust test variable name and add credential helper function Signed-off-by: Alex Goodman --- README.md | 30 ++ cmd/packages.go | 5 +- cmd/power_user.go | 2 +- go.mod | 2 +- go.sum | 6 +- internal/config/application.go | 1 + internal/config/registry.go | 72 +++++ .../snapshot/TestJSONPresenter.golden | 4 + syft/source/scheme.go | 20 +- syft/source/scheme_test.go | 263 ++++++++++++++++++ syft/source/source.go | 10 +- syft/source/source_test.go | 248 ----------------- test/cli/packages_cmd_test.go | 80 +++++- test/cli/trait_assertions_test.go | 2 +- test/integration/catalog_packages_test.go | 2 +- test/integration/utils_test.go | 4 +- 16 files changed, 471 insertions(+), 280 deletions(-) create mode 100644 internal/config/registry.go create mode 100644 syft/source/scheme_test.go diff --git a/README.md b/README.md index af0d17357..1391868de 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,16 @@ syft packages path/to/image.tar syft packages path/to/dir ``` +Sources can be explicitly provided with a scheme: +``` +docker:yourrepo/yourimage:tag use images from the Docker 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) +dir:path/to/yourproject read directly from a path on disk (any directory) +registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) +``` + The output format for Syft is configurable as well: ``` syft packages -o @@ -184,6 +194,26 @@ secrets: # SYFT_SECRETS_EXCLUDE_PATTERN_NAMES env var exclude-pattern-names: [] +# options when pulling directly from a registry via the "registry:" scheme +registry: + # skip TLS verification when communicating with the registry + # SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var + insecure-skip-tls-verify: false + + # credentials for specific registries + auth: + - # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) + # SYFT_REGISTRY_AUTH_AUTHORITY env var + authority: "" + # SYFT_REGISTRY_AUTH_USERNAME env var + username: "" + # SYFT_REGISTRY_AUTH_PASSWORD env var + password: "" + # note: token and username/password are mutually exclusive + # SYFT_REGISTRY_AUTH_TOKEN env var + token: "" + - ... # note, more credentials can be provided via config file only + log: # use structured logging # same as SYFT_LOG_STRUCTURED env var diff --git a/cmd/packages.go b/cmd/packages.go index e19bee44e..09e21a9ae 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -32,7 +32,7 @@ const ( {{.appName}} {{.command}} alpine:latest -vv show verbose debug information Supports the following image sources: - {{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon + {{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry. {{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory You can also explicitly specify the scheme to use: @@ -41,6 +41,7 @@ const ( {{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise) {{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory) + {{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) ` ) @@ -187,7 +188,7 @@ func packagesExecWorker(userInput string) <-chan error { checkForApplicationUpdate() - src, cleanup, err := source.New(userInput) + src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions()) if err != nil { errs <- fmt.Errorf("failed to determine image source: %+v", err) return diff --git a/cmd/power_user.go b/cmd/power_user.go index d44d73c68..2234d5675 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -81,7 +81,7 @@ func powerUserExecWorker(userInput string) <-chan error { checkForApplicationUpdate() - src, cleanup, err := source.New(userInput) + src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions()) if err != nil { errs <- err return diff --git a/go.mod b/go.mod index f59c9d9ba..86622dc68 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b - github.com/anchore/stereoscope v0.0.0-20210405181843-73d71fd93233 + github.com/anchore/stereoscope v0.0.0-20210412194439-0b9e0281ef0c github.com/antihax/optional v1.0.0 github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible diff --git a/go.sum b/go.sum index 9227708a8..e2d59ba67 100644 --- a/go.sum +++ b/go.sum @@ -115,10 +115,8 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= -github.com/anchore/stereoscope v0.0.0-20210323182342-47b72675ff65 h1:r3tiir6UCgj/YeTqy4s2bfhZ9SuJYNlXx1Z9e/eLrbI= -github.com/anchore/stereoscope v0.0.0-20210323182342-47b72675ff65/go.mod h1:G7tFR0iI9r6AvibmXKA9v010pRS1IIJgd0t6fOMDxCw= -github.com/anchore/stereoscope v0.0.0-20210405181843-73d71fd93233 h1:XkoyUFdQGYT2tb7SH2YBsouw/9q1kZTgXVy52PzM4JE= -github.com/anchore/stereoscope v0.0.0-20210405181843-73d71fd93233/go.mod h1:G7tFR0iI9r6AvibmXKA9v010pRS1IIJgd0t6fOMDxCw= +github.com/anchore/stereoscope v0.0.0-20210412194439-0b9e0281ef0c h1:iAkv8iBnbHQzcROt55IbEh7r7qUJxj64E8bM4EnaBlA= +github.com/anchore/stereoscope v0.0.0-20210412194439-0b9e0281ef0c/go.mod h1:vhh1M99rfWx5ejMvz1lkQiFZUrC5wu32V12R4JXH+ZI= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= diff --git a/internal/config/application.go b/internal/config/application.go index 9e0d58b0b..c8f7a1c48 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -40,6 +40,7 @@ type Application struct { FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"` FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"` Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"` + Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` } func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application { diff --git a/internal/config/registry.go b/internal/config/registry.go new file mode 100644 index 000000000..221bb5f2b --- /dev/null +++ b/internal/config/registry.go @@ -0,0 +1,72 @@ +package config + +import ( + "os" + + "github.com/anchore/stereoscope/pkg/image" + + "github.com/spf13/viper" +) + +type RegistryCredentials struct { + Authority string `yaml:"authority" json:"authority" mapstructure:"authority"` + // IMPORTANT: do not show the username in any YAML/JSON output (sensitive information) + Username string `yaml:"-" json:"-" mapstructure:"username"` + // IMPORTANT: do not show the password in any YAML/JSON output (sensitive information) + Password string `yaml:"-" json:"-" mapstructure:"password"` + // IMPORTANT: do not show the token in any YAML/JSON output (sensitive information) + Token string `yaml:"-" json:"-" mapstructure:"token"` +} + +type registry struct { + InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"` + Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"` +} + +func (cfg registry) loadDefaultValues(v *viper.Viper) { + v.SetDefault("registry.insecure-skip-tls-verify", false) + v.SetDefault("registry.auth", []RegistryCredentials{}) +} + +// nolint: unparam +func (cfg *registry) parseConfigValues() error { + // there may be additional credentials provided by env var that should be appended to the set of credentials + authority, username, password, token := + os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"), + os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"), + os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"), + os.Getenv("SYFT_REGISTRY_AUTH_TOKEN") + + if hasNonEmptyCredentials(authority, password, token) { + // note: we prepend the credentials such that the environment variables take precedence over on-disk configuration. + cfg.Auth = append([]RegistryCredentials{ + { + Authority: authority, + Username: username, + Password: password, + Token: token, + }, + }, cfg.Auth...) + } + return nil +} + +func hasNonEmptyCredentials(authority, password, token string) bool { + return authority != "" && password != "" || authority != "" && token != "" +} + +func (cfg *registry) ToOptions() *image.RegistryOptions { + var auth = make([]image.RegistryCredentials, len(cfg.Auth)) + for i, a := range cfg.Auth { + auth[i] = image.RegistryCredentials{ + Authority: a.Authority, + Username: a.Username, + Password: a.Password, + Token: a.Token, + } + } + return &image.RegistryOptions{ + InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify, + Credentials: auth, + } +} diff --git a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden index 0e92c7967..7067fa2bc 100644 --- a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden +++ b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden @@ -223,6 +223,10 @@ "exclude-pattern-names": null, "reveal-values": false, "skip-files-above-size": 0 + }, + "registry": { + "insecure-skip-tls-verify": false, + "auth": null } } }, diff --git a/syft/source/scheme.go b/syft/source/scheme.go index d2f1c73eb..94c3b433a 100644 --- a/syft/source/scheme.go +++ b/syft/source/scheme.go @@ -21,39 +21,39 @@ const ( ImageScheme Scheme = "ImageScheme" ) -func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, string, error) { +func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, image.Source, string, error) { if strings.HasPrefix(userInput, "dir:") { // blindly trust the user's scheme dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:")) if err != nil { - return UnknownScheme, "", fmt.Errorf("unable to expand directory path: %w", err) + return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err) } - return DirectoryScheme, dirLocation, nil + return DirectoryScheme, image.UnknownSource, dirLocation, nil } - // we should attempt to let stereoscope determine what the source is first --just because the source is a valid directory + // we should attempt to let stereoscope determine what the source is first --but, just because the source is a valid directory // doesn't mean we yet know if it is an OCI layout directory (to be treated as an image) or if it is a generic filesystem directory. source, imageSpec, err := imageDetector(userInput) if err != nil { - return UnknownScheme, "", fmt.Errorf("unable to detect the scheme from %q: %w", userInput, err) + return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to detect the scheme from %q: %w", userInput, err) } if source == image.UnknownSource { dirLocation, err := homedir.Expand(userInput) if err != nil { - return UnknownScheme, "", fmt.Errorf("unable to expand potential directory path: %w", err) + return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand potential directory path: %w", err) } fileMeta, err := fs.Stat(dirLocation) if err != nil { - return UnknownScheme, "", nil + return UnknownScheme, source, "", nil } if fileMeta.IsDir() { - return DirectoryScheme, dirLocation, nil + return DirectoryScheme, source, dirLocation, nil } - return UnknownScheme, "", nil + return UnknownScheme, source, "", nil } - return ImageScheme, imageSpec, nil + return ImageScheme, source, imageSpec, nil } diff --git a/syft/source/scheme_test.go b/syft/source/scheme_test.go new file mode 100644 index 000000000..465f1107d --- /dev/null +++ b/syft/source/scheme_test.go @@ -0,0 +1,263 @@ +package source + +import ( + "os" + "testing" + + "github.com/anchore/stereoscope/pkg/image" + "github.com/mitchellh/go-homedir" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestDetectScheme(t *testing.T) { + type detectorResult struct { + src image.Source + ref string + err error + } + + testCases := []struct { + name string + userInput string + dirs []string + detection detectorResult + expectedScheme Scheme + expectedLocation string + }{ + { + name: "docker-image-ref", + userInput: "wagoodman/dive:latest", + detection: detectorResult{ + src: image.DockerDaemonSource, + ref: "wagoodman/dive:latest", + }, + expectedScheme: ImageScheme, + expectedLocation: "wagoodman/dive:latest", + }, + { + name: "docker-image-ref-no-tag", + userInput: "wagoodman/dive", + detection: detectorResult{ + src: image.DockerDaemonSource, + ref: "wagoodman/dive", + }, + expectedScheme: ImageScheme, + expectedLocation: "wagoodman/dive", + }, + { + name: "registry-image-explicit-scheme", + userInput: "registry:wagoodman/dive:latest", + detection: detectorResult{ + src: image.OciRegistrySource, + ref: "wagoodman/dive:latest", + }, + expectedScheme: ImageScheme, + expectedLocation: "wagoodman/dive:latest", + }, + { + name: "docker-image-explicit-scheme", + userInput: "docker:wagoodman/dive:latest", + detection: detectorResult{ + src: image.DockerDaemonSource, + ref: "wagoodman/dive:latest", + }, + expectedScheme: ImageScheme, + expectedLocation: "wagoodman/dive:latest", + }, + { + name: "docker-image-explicit-scheme-no-tag", + userInput: "docker:wagoodman/dive", + detection: detectorResult{ + src: image.DockerDaemonSource, + ref: "wagoodman/dive", + }, + expectedScheme: ImageScheme, + expectedLocation: "wagoodman/dive", + }, + { + name: "docker-image-edge-case", + userInput: "docker:latest", + detection: detectorResult{ + src: image.DockerDaemonSource, + ref: "latest", + }, + expectedScheme: ImageScheme, + // we want 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: ImageScheme, + // we want 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: ImageScheme, + 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: ImageScheme, + 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: DirectoryScheme, + 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: ImageScheme, + expectedLocation: "some/path-to-dir", + }, + { + name: "explicit-dir", + userInput: "dir:some/path-to-dir", + detection: detectorResult{ + src: image.UnknownSource, + ref: "", + }, + dirs: []string{"some/path-to-dir"}, + expectedScheme: DirectoryScheme, + expectedLocation: "some/path-to-dir", + }, + { + name: "explicit-current-dir", + userInput: "dir:.", + detection: detectorResult{ + src: image.UnknownSource, + ref: "", + }, + expectedScheme: DirectoryScheme, + expectedLocation: ".", + }, + { + name: "current-dir", + userInput: ".", + detection: detectorResult{ + src: image.UnknownSource, + ref: "", + }, + expectedScheme: DirectoryScheme, + expectedLocation: ".", + }, + // we should support tilde expansion + { + name: "tilde-expansion-image-implicit", + userInput: "~/some-path", + detection: detectorResult{ + src: image.OciDirectorySource, + ref: "~/some-path", + }, + expectedScheme: ImageScheme, + expectedLocation: "~/some-path", + }, + { + name: "tilde-expansion-dir-implicit", + userInput: "~/some-path", + detection: detectorResult{ + src: image.UnknownSource, + ref: "", + }, + dirs: []string{"~/some-path"}, + expectedScheme: DirectoryScheme, + expectedLocation: "~/some-path", + }, + { + name: "tilde-expansion-dir-explicit-exists", + userInput: "dir:~/some-path", + dirs: []string{"~/some-path"}, + expectedScheme: DirectoryScheme, + expectedLocation: "~/some-path", + }, + { + name: "tilde-expansion-dir-explicit-dne", + userInput: "dir:~/some-path", + expectedScheme: DirectoryScheme, + expectedLocation: "~/some-path", + }, + { + name: "tilde-expansion-dir-implicit-dne", + userInput: "~/some-path", + expectedScheme: UnknownScheme, + expectedLocation: "", + }, + } + 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 tar: %+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 := detectScheme(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/source.go b/syft/source/source.go index b1e081db6..1d79dea4b 100644 --- a/syft/source/source.go +++ b/syft/source/source.go @@ -8,11 +8,9 @@ package source import ( "fmt" - "github.com/spf13/afero" - "github.com/anchore/stereoscope" - "github.com/anchore/stereoscope/pkg/image" + "github.com/spf13/afero" ) // Source is an object that captures the data source to be cataloged, configuration, and a specific resolver used @@ -25,9 +23,9 @@ type Source struct { type sourceDetector func(string) (image.Source, string, error) // New produces a Source based on userInput like dir: or image:tag -func New(userInput string) (Source, func(), error) { +func New(userInput string, registryOptions *image.RegistryOptions) (Source, func(), error) { fs := afero.NewOsFs() - parsedScheme, location, err := detectScheme(fs, image.DetectSource, userInput) + parsedScheme, imageSource, location, err := detectScheme(fs, image.DetectSource, userInput) if err != nil { return Source{}, func() {}, fmt.Errorf("unable to parse input=%q: %w", userInput, err) } @@ -50,7 +48,7 @@ func New(userInput string) (Source, func(), error) { return s, func() {}, nil case ImageScheme: - img, err := stereoscope.GetImage(location) + img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions) cleanup := func() { stereoscope.Cleanup() } diff --git a/syft/source/source_test.go b/syft/source/source_test.go index 72a97653b..11d213145 100644 --- a/syft/source/source_test.go +++ b/syft/source/source_test.go @@ -1,12 +1,9 @@ package source import ( - "os" "testing" "github.com/anchore/stereoscope/pkg/image" - "github.com/mitchellh/go-homedir" - "github.com/spf13/afero" ) func TestNewFromImageFails(t *testing.T) { @@ -173,248 +170,3 @@ func TestFilesByGlob(t *testing.T) { }) } } - -func TestDetectScheme(t *testing.T) { - type detectorResult struct { - src image.Source - ref string - err error - } - - testCases := []struct { - name string - userInput string - dirs []string - detection detectorResult - expectedScheme Scheme - expectedLocation string - }{ - { - name: "docker-image-ref", - userInput: "wagoodman/dive:latest", - detection: detectorResult{ - src: image.DockerDaemonSource, - ref: "wagoodman/dive:latest", - }, - expectedScheme: ImageScheme, - expectedLocation: "wagoodman/dive:latest", - }, - { - name: "docker-image-ref-no-tag", - userInput: "wagoodman/dive", - detection: detectorResult{ - src: image.DockerDaemonSource, - ref: "wagoodman/dive", - }, - expectedScheme: ImageScheme, - expectedLocation: "wagoodman/dive", - }, - { - name: "docker-image-explicit-scheme", - userInput: "docker:wagoodman/dive:latest", - detection: detectorResult{ - src: image.DockerDaemonSource, - ref: "wagoodman/dive:latest", - }, - expectedScheme: ImageScheme, - expectedLocation: "wagoodman/dive:latest", - }, - { - name: "docker-image-explicit-scheme-no-tag", - userInput: "docker:wagoodman/dive", - detection: detectorResult{ - src: image.DockerDaemonSource, - ref: "wagoodman/dive", - }, - expectedScheme: ImageScheme, - expectedLocation: "wagoodman/dive", - }, - { - name: "docker-image-edge-case", - userInput: "docker:latest", - detection: detectorResult{ - src: image.DockerDaemonSource, - ref: "latest", - }, - expectedScheme: ImageScheme, - // we want 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: ImageScheme, - // we want 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: ImageScheme, - 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: ImageScheme, - 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: DirectoryScheme, - 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: ImageScheme, - expectedLocation: "some/path-to-dir", - }, - { - name: "explicit-dir", - userInput: "dir:some/path-to-dir", - detection: detectorResult{ - src: image.UnknownSource, - ref: "", - }, - dirs: []string{"some/path-to-dir"}, - expectedScheme: DirectoryScheme, - expectedLocation: "some/path-to-dir", - }, - { - name: "explicit-current-dir", - userInput: "dir:.", - detection: detectorResult{ - src: image.UnknownSource, - ref: "", - }, - expectedScheme: DirectoryScheme, - expectedLocation: ".", - }, - { - name: "current-dir", - userInput: ".", - detection: detectorResult{ - src: image.UnknownSource, - ref: "", - }, - expectedScheme: DirectoryScheme, - expectedLocation: ".", - }, - // we should support tilde expansion - { - name: "tilde-expansion-image-implicit", - userInput: "~/some-path", - detection: detectorResult{ - src: image.OciDirectorySource, - ref: "~/some-path", - }, - expectedScheme: ImageScheme, - expectedLocation: "~/some-path", - }, - { - name: "tilde-expansion-dir-implicit", - userInput: "~/some-path", - detection: detectorResult{ - src: image.UnknownSource, - ref: "", - }, - dirs: []string{"~/some-path"}, - expectedScheme: DirectoryScheme, - expectedLocation: "~/some-path", - }, - { - name: "tilde-expansion-dir-explicit-exists", - userInput: "dir:~/some-path", - dirs: []string{"~/some-path"}, - expectedScheme: DirectoryScheme, - expectedLocation: "~/some-path", - }, - { - name: "tilde-expansion-dir-explicit-dne", - userInput: "dir:~/some-path", - expectedScheme: DirectoryScheme, - expectedLocation: "~/some-path", - }, - { - name: "tilde-expansion-dir-implicit-dne", - userInput: "~/some-path", - expectedScheme: UnknownScheme, - expectedLocation: "", - }, - } - 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 tar: %+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, actualLocation, err := detectScheme(fs, imageDetector, test.userInput) - if err != nil { - t.Fatalf("unexpected err : %+v", err) - } - - if actualScheme != test.expectedScheme { - t.Errorf("expected scheme %q , got %q", test.expectedScheme, actualScheme) - } - - // 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) - } - - if actualLocation != expandedExpectedLocation { - t.Errorf("expected location %q , got %q", expandedExpectedLocation, actualLocation) - } - }) - } -} diff --git a/test/cli/packages_cmd_test.go b/test/cli/packages_cmd_test.go index e5c28dfbb..83fe96724 100644 --- a/test/cli/packages_cmd_test.go +++ b/test/cli/packages_cmd_test.go @@ -21,7 +21,7 @@ func TestPackagesCmdFlags(t *testing.T) { args: []string{"packages", "-o", "json", request}, assertions: []traitAssertion{ assertJsonReport, - assertSource(source.SquashedScope), + assertScope(source.SquashedScope), assertSuccessfulReturnCode, }, }, @@ -56,7 +56,7 @@ func TestPackagesCmdFlags(t *testing.T) { name: "squashed-scope-flag", args: []string{"packages", "-o", "json", "-s", "squashed", request}, assertions: []traitAssertion{ - assertSource(source.SquashedScope), + assertScope(source.SquashedScope), assertSuccessfulReturnCode, }, }, @@ -64,7 +64,7 @@ func TestPackagesCmdFlags(t *testing.T) { name: "all-layers-scope-flag", args: []string{"packages", "-o", "json", "-s", "all-layers", request}, assertions: []traitAssertion{ - assertSource(source.AllLayersScope), + assertScope(source.AllLayersScope), assertSuccessfulReturnCode, }, }, @@ -75,7 +75,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, args: []string{"packages", "-o", "json", request}, assertions: []traitAssertion{ - assertSource(source.AllLayersScope), + assertScope(source.AllLayersScope), assertSuccessfulReturnCode, }, }, @@ -137,3 +137,75 @@ func TestPackagesCmdFlags(t *testing.T) { }) } } + +func TestRegistryAuth(t *testing.T) { + tests := []struct { + name string + args []string + env map[string]string + assertions []traitAssertion + }{ + { + name: "fallback to keychain", + args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"}, + assertions: []traitAssertion{ + assertInOutput("source=OciRegistry"), + assertInOutput("localhost:5000/something:latest"), + assertInOutput("no registry credentials configured, using the default keychain"), + }, + }, + { + name: "use creds", + args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"}, + env: map[string]string{ + "SYFT_REGISTRY_AUTH_AUTHORITY": "localhost:5000", + "SYFT_REGISTRY_AUTH_USERNAME": "username", + "SYFT_REGISTRY_AUTH_PASSWORD": "password", + }, + assertions: []traitAssertion{ + assertInOutput("source=OciRegistry"), + assertInOutput("localhost:5000/something:latest"), + assertInOutput(`using registry credentials for "localhost:5000"`), + }, + }, + { + name: "use token", + args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"}, + env: map[string]string{ + "SYFT_REGISTRY_AUTH_AUTHORITY": "localhost:5000", + "SYFT_REGISTRY_AUTH_TOKEN": "token", + }, + assertions: []traitAssertion{ + assertInOutput("source=OciRegistry"), + assertInOutput("localhost:5000/something:latest"), + assertInOutput(`using registry token for "localhost:5000"`), + }, + }, + { + name: "not enough info fallsback to keychain", + args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"}, + env: map[string]string{ + "SYFT_REGISTRY_AUTH_AUTHORITY": "localhost:5000", + }, + assertions: []traitAssertion{ + assertInOutput("source=OciRegistry"), + assertInOutput("localhost:5000/something:latest"), + assertInOutput(`no registry credentials configured, using the default keychain`), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd, stdout, stderr := runSyftCommand(t, test.env, test.args...) + for _, traitAssertionFn := range test.assertions { + traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) + } + if t.Failed() { + t.Log("STDOUT:\n", stdout) + t.Log("STDERR:\n", stderr) + t.Log("COMMAND:", strings.Join(cmd.Args, " ")) + } + }) + } +} diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go index 1eb1b807e..465149999 100644 --- a/test/cli/trait_assertions_test.go +++ b/test/cli/trait_assertions_test.go @@ -27,7 +27,7 @@ func assertTableReport(tb testing.TB, stdout, _ string, _ int) { } } -func assertSource(scope source.Scope) traitAssertion { +func assertScope(scope source.Scope) traitAssertion { return func(tb testing.TB, stdout, stderr string, rc int) { // we can only verify source with the json report assertJsonReport(tb, stdout, stderr, rc) diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 33df9c172..0a7299e18 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -23,7 +23,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) { var pc *pkg.Catalog for _, c := range cataloger.ImageCatalogers() { // in case of future alteration where state is persisted, assume no dependency is safe to reuse - theSource, cleanupSource, err := source.New("docker-archive:" + tarPath) + theSource, cleanupSource, err := source.New("docker-archive:"+tarPath, nil) b.Cleanup(cleanupSource) if err != nil { b.Fatalf("unable to get source: %+v", err) diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index ad194b273..56cbc7b09 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -14,7 +14,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (*pkg.Catalog, * imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) - theSource, cleanupSource, err := source.New("docker-archive:" + tarPath) + theSource, cleanupSource, err := source.New("docker-archive:"+tarPath, nil) t.Cleanup(cleanupSource) if err != nil { t.Fatalf("unable to get source: %+v", err) @@ -29,7 +29,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (*pkg.Catalog, * } func catalogDirectory(t *testing.T, dir string) (*pkg.Catalog, *distro.Distro, source.Source) { - theSource, cleanupSource, err := source.New("dir:" + dir) + theSource, cleanupSource, err := source.New("dir:"+dir, nil) t.Cleanup(cleanupSource) if err != nil { t.Fatalf("unable to get source: %+v", err)