Add ability to pull images directly from a registry (#378)

* add registry image source

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* use explicit source for fetching image + add scheme and registry tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* adjust test variable name and add credential helper function

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2021-04-13 09:30:57 -04:00 committed by GitHub
parent c02ab88d5f
commit c363b2b532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 471 additions and 280 deletions

View File

@ -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 <image> -o <format>
@ -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

View File

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

View File

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

2
go.mod
View File

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

6
go.sum
View File

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

View File

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

View File

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

View File

@ -223,6 +223,10 @@
"exclude-pattern-names": null,
"reveal-values": false,
"skip-files-above-size": 0
},
"registry": {
"insecure-skip-tls-verify": false,
"auth": null
}
}
},

View File

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

263
syft/source/scheme_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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