chore: update CLI to CLIO (#2001)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Keith Zantow
2023-08-29 15:52:26 -04:00
committed by GitHub
co-authored by Alex Goodman
parent b03e9c6868
commit 2b7a9d0be3
95 changed files with 1680 additions and 3192 deletions
+8 -5
View File
@@ -3,21 +3,24 @@ package bus
import (
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/redact"
"github.com/anchore/syft/syft/event"
)
func Exit() {
Publish(partybus.Event{
Type: event.CLIExit,
})
Publish(clio.ExitEvent(false))
}
func ExitWithInterrupt() {
Publish(clio.ExitEvent(true))
}
func Report(report string) {
if len(report) == 0 {
return
}
report = log.Redactor.RedactString(report)
report = redact.Apply(report)
Publish(partybus.Event{
Type: event.CLIReport,
Value: report,
-337
View File
@@ -1,337 +0,0 @@
package config
import (
"errors"
"fmt"
"os"
"path"
"reflect"
"sort"
"strings"
"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
"github.com/anchore/go-logger"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg/cataloger"
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python"
)
var (
ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
catalogerEnabledDefault = false
)
type defaultValueLoader interface {
loadDefaultValues(*viper.Viper)
}
type parser interface {
parseConfigValues() error
}
// Application is the main syft application configuration.
type Application struct {
// the location where the application config was read from (either from -c or discovered while loading); default .syft.yaml
ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"`
Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"`
// -q, indicates to not show any status output to stderr (ETUI or logging UI)
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"`
LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
Python python `yaml:"python" json:"python" mapstructure:"python"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
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"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files
}
func (cfg Application) ToCatalogerConfig() cataloger.Config {
return cataloger.Config{
Search: cataloger.SearchConfig{
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
Scope: cfg.Package.Cataloger.ScopeOpt,
},
Catalogers: cfg.Catalogers,
Parallelism: cfg.Parallelism,
ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership,
Golang: golangCataloger.NewGoCatalogerOpts().
WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
WithProxy(cfg.Golang.Proxy).
WithNoProxy(cfg.Golang.NoProxy),
LinuxKernel: kernel.LinuxCatalogerConfig{
CatalogModules: cfg.LinuxKernel.CatalogModules,
},
Python: pythonCataloger.CatalogerConfig{
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
},
}
}
func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error {
// priority order: viper.Set, flag, env, config, kv, defaults
// flags have already been loaded into viper by command construction
// check if user specified config; otherwise read all possible paths
if err := loadConfig(v, configPath); err != nil {
var notFound *viper.ConfigFileNotFoundError
if errors.As(err, &notFound) {
log.Debugf("no config file found, using defaults")
} else {
return fmt.Errorf("unable to load config: %w", err)
}
}
// load default config values into viper
loadDefaultValues(v)
// load environment variables
v.SetEnvPrefix(internal.ApplicationName)
v.AllowEmptyEnv(true)
v.AutomaticEnv()
// unmarshal fully populated viper object onto config
err := v.Unmarshal(cfg)
if err != nil {
return err
}
// Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope
return cfg.parseConfigValues()
}
func (cfg *Application) parseConfigValues() error {
// parse options on this struct
var catalogers []string
for _, c := range cfg.Catalogers {
for _, f := range strings.Split(c, ",") {
catalogers = append(catalogers, strings.TrimSpace(f))
}
}
sort.Strings(catalogers)
cfg.Catalogers = catalogers
// parse application config options
for _, optionFn := range []func() error{
cfg.parseLogLevelOption,
cfg.parseFile,
} {
if err := optionFn(); err != nil {
return err
}
}
if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil {
return err
}
if cfg.Name != "" {
log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version")
if cfg.Source.Name == "" {
cfg.Source.Name = cfg.Name
}
}
// check for valid default source options
// parse nested config options
// for each field in the configuration struct, see if the field implements the parser interface
// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
value := reflect.ValueOf(cfg).Elem()
for i := 0; i < value.NumField(); i++ {
// note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer.
if parsable, ok := value.Field(i).Addr().Interface().(parser); ok {
// the field implements parser, call it
if err := parsable.parseConfigValues(); err != nil {
return err
}
}
}
return nil
}
func (cfg *Application) parseLogLevelOption() error {
switch {
case cfg.Quiet:
// TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.Level = logger.DisabledLevel
case cfg.Verbosity > 0:
cfg.Log.Level = logger.LevelFromVerbosity(int(cfg.Verbosity), logger.WarnLevel, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel)
case cfg.Log.Level != "":
var err error
cfg.Log.Level, err = logger.LevelFromString(string(cfg.Log.Level))
if err != nil {
return err
}
if logger.IsVerbose(cfg.Log.Level) {
cfg.Verbosity = 1
}
default:
cfg.Log.Level = logger.WarnLevel
}
return nil
}
func (cfg *Application) parseFile() error {
if cfg.File != "" {
expandedPath, err := homedir.Expand(cfg.File)
if err != nil {
return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err)
}
cfg.File = expandedPath
}
return nil
}
// init loads the default configuration values into the viper instance (before the config values are read and parsed).
func loadDefaultValues(v *viper.Viper) {
// set the default values for primitive fields in this struct
v.SetDefault("quiet", false)
v.SetDefault("check-for-app-update", true)
v.SetDefault("catalogers", nil)
v.SetDefault("parallelism", 1)
v.SetDefault("default-image-pull-source", "")
v.SetDefault("exclude-binary-overlap-by-ownership", true)
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
value := reflect.ValueOf(Application{})
for i := 0; i < value.NumField(); i++ {
// note: the defaultValueLoader method receiver is NOT a pointer receiver.
if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
// the field implements defaultValueLoader, call it
loadable.loadDefaultValues(v)
}
}
}
func (cfg Application) String() string {
// yaml is pretty human friendly (at least when compared to json)
appaStr, err := yaml.Marshal(&cfg)
if err != nil {
return err.Error()
}
return string(appaStr)
}
// nolint:funlen
func loadConfig(v *viper.Viper, configPath string) error {
var err error
// use explicitly the given user config
if configPath != "" {
v.SetConfigFile(configPath)
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("unable to read application config=%q : %w", configPath, err)
}
v.Set("config", v.ConfigFileUsed())
// don't fall through to other options if the config path was explicitly provided
return nil
}
// start searching for valid configs in order...
// 1. look for .<appname>.yaml (in the current directory)
confFilePath := "." + internal.ApplicationName
// TODO: Remove this before v1.0.0
// See syft #1634
v.AddConfigPath(".")
v.SetConfigName(confFilePath)
// check if config.yaml exists in the current directory
// DEPRECATED: this will be removed in v1.0.0
if _, err := os.Stat("config.yaml"); err == nil {
log.Warn("DEPRECATED: ./config.yaml as a configuration file is deprecated and will be removed as an option in v1.0.0, please rename to .syft.yaml")
}
if _, err := os.Stat(confFilePath + ".yaml"); err == nil {
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
}
// 2. look for .<appname>/config.yaml (in the current directory)
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
// 3. look for ~/.<appname>.yaml
home, err := homedir.Dir()
if err == nil {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
}
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
v.SetConfigName("config")
configPath = path.Join(xdg.ConfigHome, internal.ApplicationName)
v.AddConfigPath(configPath)
for _, dir := range xdg.ConfigDirs {
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
}
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
return nil
}
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
func checkDefaultSourceValues(source string) error {
validValues := internal.NewStringSet(validDefaultSourceValues...)
if !validValues.Contains(source) {
validValuesString := strings.Join(validDefaultSourceValues, ", ")
return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
}
return nil
}
-129
View File
@@ -1,129 +0,0 @@
package config
import (
"fmt"
"os"
"path"
"testing"
"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO: set negative case when config.yaml is no longer a valid option
func TestApplicationConfig(t *testing.T) {
// disable homedir package cache for testing
originalCacheOpt := homedir.DisableCache
homedir.DisableCache = true
t.Cleanup(func() {
homedir.DisableCache = originalCacheOpt
})
// ensure we have no side effects for xdg package for future tests
originalXDG := os.Getenv("XDG_CONFIG_HOME")
t.Cleanup(func() {
// note: we're not using t.Setenv since the effect we're trying to eliminate is within the xdg package
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", originalXDG))
xdg.Reload()
})
// config is picked up at desired configuration paths
// VALID: .syft.yaml, .syft/config.yaml, ~/.syft.yaml, <XDG_CONFIG_HOME>/syft/config.yaml
// DEPRECATED: config.yaml is currently supported by
tests := []struct {
name string
setup func(t *testing.T) string
assertions func(t *testing.T, app *Application)
cleanup func()
}{
{
name: "explicit config",
setup: func(t *testing.T) string {
return "./test-fixtures/.syft.yaml"
}, // no-op for explicit config
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-explicit-config", app.File)
},
},
{
name: "current working directory named config",
setup: func(t *testing.T) string {
err := os.Chdir("./test-fixtures/config-wd-file") // change application cwd to test-fixtures
require.NoError(t, err)
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-wd-named-config", app.File)
},
},
{
name: "current working directory syft dir config",
setup: func(t *testing.T) string {
err := os.Chdir("./test-fixtures/config-dir-test") // change application cwd to test-fixtures
require.NoError(t, err)
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-dir-config", app.File)
},
},
{
name: "home directory file config",
setup: func(t *testing.T) string {
// Because Setenv affects the whole process, it cannot be used in parallel tests or
// tests with parallel ancestors: see separate XDG test for consequence of this
t.Setenv("HOME", "./test-fixtures/config-home-test/config-file")
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-home-config", app.File)
},
},
{
name: "XDG file config",
setup: func(t *testing.T) string {
wd, err := os.Getwd()
require.NoError(t, err)
configDir := path.Join(wd, "./test-fixtures/config-home-test") // set HOME to testdata
// note: this explicitly has multiple XDG paths, make certain we use the first VALID one (not the first one)
t.Setenv("XDG_CONFIG_DIRS", fmt.Sprintf("/another/foo/bar:%s", configDir))
xdg.Reload()
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-home-XDG-config", app.File)
},
cleanup: func() {
require.NoError(t, os.Unsetenv("XDG_CONFIG_DIRS"))
xdg.Reload()
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.cleanup != nil {
t.Cleanup(test.cleanup)
}
wd, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(wd) // reset working directory after test
application := &Application{}
viperInstance := viper.New()
// this will override home in case you are running this test locally and DO have a syft config
// in your home directory... now it will be ignored. Same for XDG_CONFIG_DIRS.
t.Setenv("HOME", "/foo/bar")
t.Setenv("XDG_CONFIG_DIRS", "/foo/bar")
xdg.Reload()
configPath := test.setup(t)
err = application.LoadAllValues(viperInstance, configPath)
require.NoError(t, err)
test.assertions(t, application)
})
}
}
-14
View File
@@ -1,14 +0,0 @@
package config
import "github.com/spf13/viper"
type attest struct {
// IMPORTANT: do not show the attestation key/password in any YAML/JSON output (sensitive information)
Key string `yaml:"-" json:"-" mapstructure:"key"`
Password string `yaml:"-" json:"-" mapstructure:"password"`
}
func (cfg attest) loadDefaultValues(v *viper.Viper) {
v.SetDefault("attest.key", "")
v.SetDefault("attest.password", "")
}
-29
View File
@@ -1,29 +0,0 @@
package config
import (
"fmt"
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type catalogerOptions struct {
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
ScopeOpt source.Scope `yaml:"-" json:"-"`
}
func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) {
v.SetDefault("package.cataloger.enabled", true)
}
func (cfg *catalogerOptions) parseConfigValues() error {
scopeOption := source.ParseScope(cfg.Scope)
if scopeOption == source.UnknownScope {
return fmt.Errorf("bad scope value %q", cfg.Scope)
}
cfg.ScopeOpt = scopeOption
return nil
}
-13
View File
@@ -1,13 +0,0 @@
package config
import "github.com/spf13/viper"
type development struct {
ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"`
ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"`
}
func (cfg development) loadDefaultValues(v *viper.Viper) {
v.SetDefault("dev.profile-cpu", false)
v.SetDefault("dev.profile-mem", false)
}
-20
View File
@@ -1,20 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type fileClassification struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
}
func (cfg *fileClassification) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-25
View File
@@ -1,25 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
)
type fileContents struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
}
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{})
}
func (cfg *fileContents) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-22
View File
@@ -1,22 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type FileMetadata struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
}
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"})
}
func (cfg *FileMetadata) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-19
View File
@@ -1,19 +0,0 @@
package config
import "github.com/spf13/viper"
type golang struct {
SearchLocalModCacheLicenses bool `json:"search-local-mod-cache-licenses" yaml:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
LocalModCacheDir string `json:"local-mod-cache-dir" yaml:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"`
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
}
func (cfg golang) loadDefaultValues(v *viper.Viper) {
v.SetDefault("golang.search-local-mod-cache-licenses", false)
v.SetDefault("golang.local-mod-cache-dir", "")
v.SetDefault("golang.search-remote-licenses", false)
v.SetDefault("golang.proxy", "")
v.SetDefault("golang.no-proxy", "")
}
-11
View File
@@ -1,11 +0,0 @@
package config
import "github.com/spf13/viper"
type linuxKernel struct {
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
}
func (cfg linuxKernel) loadDefaultValues(v *viper.Viper) {
v.SetDefault("linux-kernel.catalog-modules", true)
}
-34
View File
@@ -1,34 +0,0 @@
package config
import (
"fmt"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"github.com/anchore/go-logger"
)
// logging contains all logging-related configuration options available to the user via the application config.
type logging struct {
Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
Level logger.Level `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint
FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to
}
func (cfg *logging) parseConfigValues() error {
if cfg.FileLocation != "" {
expandedPath, err := homedir.Expand(cfg.FileLocation)
if err != nil {
return fmt.Errorf("unable to expand log file path=%q: %w", cfg.FileLocation, err)
}
cfg.FileLocation = expandedPath
}
return nil
}
func (cfg logging) loadDefaultValues(v *viper.Viper) {
v.SetDefault("log.structured", false)
v.SetDefault("log.file", "")
v.SetDefault("log.level", string(logger.WarnLevel))
}
-24
View File
@@ -1,24 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/pkg/cataloger"
)
type pkg struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
}
func (cfg pkg) loadDefaultValues(v *viper.Viper) {
cfg.Cataloger.loadDefaultValues(v)
c := cataloger.DefaultSearchConfig()
v.SetDefault("package.search-unindexed-archives", c.IncludeUnindexedArchives)
v.SetDefault("package.search-indexed-archives", c.IncludeIndexedArchives)
}
func (cfg *pkg) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-13
View File
@@ -1,13 +0,0 @@
package config
import (
"github.com/spf13/viper"
)
type python struct {
GuessUnpinnedRequirements bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
}
func (cfg python) loadDefaultValues(v *viper.Viper) {
v.SetDefault("python.guess-unpinned-requirements", false)
}
-91
View File
@@ -1,91 +0,0 @@
package config
import (
"os"
"github.com/spf13/viper"
"github.com/anchore/stereoscope/pkg/image"
)
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"`
TLSCert string `yaml:"tls-cert,omitempty" json:"tls-cert,omitempty" mapstructure:"tls-cert"`
TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"`
}
type registry struct {
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"`
}
func (cfg registry) loadDefaultValues(v *viper.Viper) {
v.SetDefault("registry.insecure-skip-tls-verify", false)
v.SetDefault("registry.insecure-use-http", false)
v.SetDefault("registry.auth", []RegistryCredentials{})
v.SetDefault("registry.ca-cert", "")
}
//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, tlsCert, tlsKey :=
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"),
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN"),
os.Getenv("SYFT_REGISTRY_AUTH_TLS_CERT"),
os.Getenv("SYFT_REGISTRY_AUTH_TLS_KEY")
if hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) {
// 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,
TLSCert: tlsCert,
TLSKey: tlsKey,
},
}, cfg.Auth...)
}
return nil
}
func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool {
hasUserPass := username != "" && password != ""
hasToken := token != ""
hasTLSMaterial := tlsCert != "" && tlsKey != ""
return hasUserPass || hasToken || hasTLSMaterial
}
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,
ClientCert: a.TLSCert,
ClientKey: a.TLSKey,
}
}
return &image.RegistryOptions{
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
InsecureUseHTTP: cfg.InsecureUseHTTP,
Credentials: auth,
CAFileOrDir: cfg.CACert,
}
}
-147
View File
@@ -1,147 +0,0 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/image"
)
func TestHasNonEmptyCredentials(t *testing.T) {
tests := []struct {
username, password, token, cert, key string
expected bool
}{
{
"", "", "", "", "",
false,
},
{
"user", "", "", "", "",
false,
},
{
"", "pass", "", "", "",
false,
},
{
"", "pass", "tok", "", "",
true,
},
{
"user", "", "tok", "", "",
true,
},
{
"", "", "tok", "", "",
true,
},
{
"user", "pass", "tok", "", "",
true,
},
{
"user", "pass", "", "", "",
true,
},
{
"", "", "", "cert", "key",
true,
},
{
"", "", "", "cert", "",
false,
},
{
"", "", "", "", "key",
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key))
})
}
}
func Test_registry_ToOptions(t *testing.T) {
tests := []struct {
name string
input registry
expected image.RegistryOptions
}{
{
name: "no registry options",
input: registry{},
expected: image.RegistryOptions{
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set InsecureSkipTLSVerify",
input: registry{
InsecureSkipTLSVerify: true,
},
expected: image.RegistryOptions{
InsecureSkipTLSVerify: true,
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set InsecureUseHTTP",
input: registry{
InsecureUseHTTP: true,
},
expected: image.RegistryOptions{
InsecureUseHTTP: true,
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set all bool options",
input: registry{
InsecureSkipTLSVerify: true,
InsecureUseHTTP: true,
},
expected: image.RegistryOptions{
InsecureSkipTLSVerify: true,
InsecureUseHTTP: true,
Credentials: []image.RegistryCredentials{},
},
},
{
name: "provide all tls configuration",
input: registry{
CACert: "ca.crt",
InsecureSkipTLSVerify: true,
Auth: []RegistryCredentials{
{
TLSCert: "client.crt",
TLSKey: "client.key",
},
},
},
expected: image.RegistryOptions{
CAFileOrDir: "ca.crt",
InsecureSkipTLSVerify: true,
Credentials: []image.RegistryCredentials{
{
ClientCert: "client.crt",
ClientKey: "client.key",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, &test.expected, test.input.ToOptions())
})
}
}
-29
View File
@@ -1,29 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
)
type secrets struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
AdditionalPatterns map[string]string `yaml:"additional-patterns" json:"additional-patterns" mapstructure:"additional-patterns"`
ExcludePatternNames []string `yaml:"exclude-pattern-names" json:"exclude-pattern-names" mapstructure:"exclude-pattern-names"`
RevealValues bool `yaml:"reveal-values" json:"reveal-values" mapstructure:"reveal-values"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
}
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
v.SetDefault("secrets.additional-patterns", map[string]string{})
v.SetDefault("secrets.exclude-pattern-names", []string{})
}
func (cfg *secrets) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-17
View File
@@ -1,17 +0,0 @@
package config
import "github.com/spf13/viper"
type sourceCfg struct {
Name string `json:"name" yaml:"name" mapstructure:"name"`
Version string `json:"version" yaml:"version" mapstructure:"version"`
File fileSource `json:"file" yaml:"file" mapstructure:"file"`
}
type fileSource struct {
Digests []string `json:"digests" yaml:"digests" mapstructure:"digests"`
}
func (cfg sourceCfg) loadDefaultValues(v *viper.Viper) {
v.SetDefault("source.file.digests", []string{"sha256"})
}
-7
View File
@@ -1,7 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-explicit-config"
package:
cataloger:
scope: "squashed"
# same as --scope; limit the scope of the cataloger to only the specified types
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-dir-config"
package:
cataloger:
scope: "squashed"
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-home-config"
package:
cataloger:
scope: "squashed"
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-home-XDG-config"
package:
cataloger:
scope: "squashed"
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-wd-named-config"
package:
cataloger:
scope: "squashed"
-3
View File
@@ -1,9 +1,6 @@
package internal
const (
// ApplicationName is the non-capitalized name of the application (do not change this)
ApplicationName = "syft"
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "10.0.1"
+12 -13
View File
@@ -7,29 +7,28 @@ import (
"github.com/anchore/go-logger"
"github.com/anchore/go-logger/adapter/discard"
"github.com/anchore/go-logger/adapter/redact"
red "github.com/anchore/syft/internal/redact"
)
var (
// log is the singleton used to facilitate logging internally within
log = discard.New()
store = redact.NewStore()
Redactor = store.(redact.Redactor)
)
// log is the singleton used to facilitate logging internally within
var log = discard.New()
func Set(l logger.Logger) {
log = redact.New(l, store)
// though the application will automatically have a redaction logger, library consumers may not be doing this.
// for this reason we additionally ensure there is a redaction logger configured for any logger passed. The
// source of truth for redaction values is still in the internal redact package. If the passed logger is already
// redacted, then this is a no-op.
store := red.Get()
if store != nil {
l = redact.New(l, store)
}
log = l
}
func Get() logger.Logger {
return log
}
func Redact(values ...string) {
store.Add(values...)
}
// Errorf takes a formatted template string and template arguments for the error logging level.
func Errorf(format string, args ...interface{}) {
log.Errorf(format, args...)
+36
View File
@@ -0,0 +1,36 @@
package redact
import "github.com/anchore/go-logger/adapter/redact"
var store redact.Store
func Set(s redact.Store) {
if store != nil {
// if someone is trying to set a redaction store and we already have one then something is wrong. The store
// that we're replacing might already have values in it, so we should never replace it.
panic("replace existing redaction store (probably unintentional)")
}
store = s
}
func Get() redact.Store {
return store
}
func Add(vs ...string) {
if store == nil {
// if someone is trying to add values that should never be output and we don't have a store then something is wrong.
// we should never accidentally output values that should be redacted, thus we panic here.
panic("cannot add redactions without a store")
}
store.Add(vs...)
}
func Apply(value string) string {
if store == nil {
// if someone is trying to add values that should never be output and we don't have a store then something is wrong.
// we should never accidentally output values that should be redacted, thus we panic here.
panic("cannot apply redactions without a store")
}
return store.RedactString(value)
}
-54
View File
@@ -1,54 +0,0 @@
/*
Package version contains all build time metadata (version, build time, git commit, etc).
*/
package version
import (
"fmt"
"runtime"
"strings"
"github.com/anchore/syft/internal"
)
const valueNotProvided = "[not provided]"
// all variables here are provided as build-time arguments, with clear default values
var version = valueNotProvided
var gitCommit = valueNotProvided
var gitDescription = valueNotProvided
var buildDate = valueNotProvided
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
// Version defines the application version details (generally from build information)
type Version struct {
Version string `json:"version"` // application semantic version
JSONSchemaVersion string `json:"jsonSchemaVersion"` // application semantic JSON schema version
GitCommit string `json:"gitCommit"` // git SHA at build-time
GitDescription string `json:"gitDescription"` // output of 'git describe --dirty --always --tags'
BuildDate string `json:"buildDate"` // date of the build
GoVersion string `json:"goVersion"` // go runtime version at build-time
Compiler string `json:"compiler"` // compiler used at build-time
Platform string `json:"platform"` // GOOS and GOARCH at build-time
}
func (v Version) IsProductionBuild() bool {
if strings.Contains(v.Version, "SNAPSHOT") || strings.Contains(v.Version, valueNotProvided) {
return false
}
return true
}
// FromBuild provides all version details
func FromBuild() Version {
return Version{
Version: version,
JSONSchemaVersion: internal.JSONSchemaVersion,
GitCommit: gitCommit,
GitDescription: gitDescription,
BuildDate: buildDate,
GoVersion: runtime.Version(),
Compiler: runtime.Compiler,
Platform: platform,
}
}
-73
View File
@@ -1,73 +0,0 @@
package version
import (
"fmt"
"io"
"net/http"
"strings"
hashiVersion "github.com/anchore/go-version"
"github.com/anchore/syft/internal"
)
var latestAppVersionURL = struct {
host string
path string
}{
host: "https://toolbox-data.anchore.io",
path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.ApplicationName),
}
// IsUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
func IsUpdateAvailable() (bool, string, error) {
currentBuildInfo := FromBuild()
if !currentBuildInfo.IsProductionBuild() {
// don't allow for non-production builds to check for a version.
return false, "", nil
}
currentVersion, err := hashiVersion.NewVersion(currentBuildInfo.Version)
if err != nil {
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
}
latestVersion, err := fetchLatestApplicationVersion()
if err != nil {
return false, "", err
}
if latestVersion.GreaterThan(currentVersion) {
return true, latestVersion.String(), nil
}
return false, "", nil
}
func fetchLatestApplicationVersion() (*hashiVersion.Version, error) {
req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for latest version: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch latest version: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status)
}
versionBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read latest version: %w", err)
}
versionStr := strings.TrimSuffix(string(versionBytes), "\n")
if len(versionStr) > 50 {
return nil, fmt.Errorf("version too long: %q", versionStr[:50])
}
return hashiVersion.NewVersion(versionStr)
}
-218
View File
@@ -1,218 +0,0 @@
package version
import (
"net/http"
"net/http/httptest"
"testing"
hashiVersion "github.com/anchore/go-version"
)
func TestIsUpdateAvailable(t *testing.T) {
tests := []struct {
name string
buildVersion string
latestVersion string
code int
isAvailable bool
newVersion string
err bool
}{
{
name: "equal",
buildVersion: "1.0.0",
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "hasUpdate",
buildVersion: "1.0.0",
latestVersion: "1.2.0",
code: 200,
isAvailable: true,
newVersion: "1.2.0",
err: false,
},
{
name: "aheadOfLatest",
buildVersion: "1.2.0",
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "EmptyUpdate",
buildVersion: "1.0.0",
latestVersion: "",
code: 200,
isAvailable: false,
newVersion: "",
err: true,
},
{
name: "GarbageUpdate",
buildVersion: "1.0.0",
latestVersion: "hdfjksdhfhkj",
code: 200,
isAvailable: false,
newVersion: "",
err: true,
},
{
name: "BadUpdate",
buildVersion: "1.0.0",
latestVersion: "1.0.",
code: 500,
isAvailable: false,
newVersion: "",
err: true,
},
{
name: "NoBuildVersion",
buildVersion: valueNotProvided,
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "SnapshotBuildVersion",
buildVersion: "2.0.0-SHAPSHOT-a78bf9c",
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "BadUpdateValidVersion",
buildVersion: "1.0.0",
latestVersion: "2.0.0",
code: 404,
isAvailable: false,
newVersion: "",
err: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// setup mocks
// local...
version = test.buildVersion
// remote...
handler := http.NewServeMux()
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.code)
_, _ = w.Write([]byte(test.latestVersion))
})
mockSrv := httptest.NewServer(handler)
latestAppVersionURL.host = mockSrv.URL
defer mockSrv.Close()
isAvailable, newVersion, err := IsUpdateAvailable()
if err != nil && !test.err {
t.Fatalf("got error but expected none: %+v", err)
} else if err == nil && test.err {
t.Fatalf("expected error but got none")
}
if newVersion != test.newVersion {
t.Errorf("unexpected NEW version: %+v", newVersion)
}
if isAvailable != test.isAvailable {
t.Errorf("unexpected result: %+v", isAvailable)
}
})
}
}
func TestFetchLatestApplicationVersion(t *testing.T) {
tests := []struct {
name string
response string
code int
err bool
expected *hashiVersion.Version
}{
{
name: "gocase",
response: "1.0.0",
code: 200,
expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")),
},
{
name: "garbage",
response: "garbage",
code: 200,
expected: nil,
err: true,
},
{
name: "http 500",
response: "1.0.0",
code: 500,
expected: nil,
err: true,
},
{
name: "http 404",
response: "1.0.0",
code: 404,
expected: nil,
err: true,
},
{
name: "empty",
response: "",
code: 200,
expected: nil,
err: true,
},
{
name: "too long",
response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ",
code: 200,
expected: nil,
err: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// setup mock
handler := http.NewServeMux()
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.code)
_, _ = w.Write([]byte(test.response))
})
mockSrv := httptest.NewServer(handler)
latestAppVersionURL.host = mockSrv.URL
defer mockSrv.Close()
actual, err := fetchLatestApplicationVersion()
if err != nil && !test.err {
t.Fatalf("got error but expected none: %+v", err)
} else if err == nil && test.err {
t.Fatalf("expected error but got none")
}
if err != nil {
return
}
if actual.String() != test.expected.String() {
t.Errorf("unexpected version: %+v", actual.String())
}
})
}
}