diff --git a/cmd/grype/cli/cli.go b/cmd/grype/cli/cli.go index 924cab85722..a2022ef7f58 100644 --- a/cmd/grype/cli/cli.go +++ b/cmd/grype/cli/cli.go @@ -12,6 +12,7 @@ import ( "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/commands" + "github.com/anchore/grype/cmd/grype/cli/internal/configwarn" grypeHandler "github.com/anchore/grype/cmd/grype/cli/ui" "github.com/anchore/grype/cmd/grype/internal/ui" v6 "github.com/anchore/grype/grype/db/v6" @@ -74,6 +75,10 @@ func SetupConfig(id clio.Identification) *clio.SetupConfig { syft.SetLogger(state.Logger.Nested("from", "syft")) stereoscope.SetLogger(state.Logger.Nested("from", "stereoscope")) + if path := configwarn.Detect(id.Name, os.Args, os.Getenv); path != "" { + state.Logger.Warnf("using configuration file from current directory: %s", path) + } + return nil }, ). diff --git a/cmd/grype/cli/internal/configwarn/configwarn.go b/cmd/grype/cli/internal/configwarn/configwarn.go new file mode 100644 index 00000000000..1baff55584c --- /dev/null +++ b/cmd/grype/cli/internal/configwarn/configwarn.go @@ -0,0 +1,85 @@ +// Package configwarn detects when grype is about to pick up a configuration +// file from the current working directory (e.g. ./.grype.yaml) without an +// explicit --config flag or GRYPE_CONFIG environment variable. Reading a +// hidden config file from the CWD silently can be a debugging hazard +// (see anchore/grype#3427), so we surface a WARN-level log when it happens. +package configwarn + +import ( + "os" + "path/filepath" + "strings" +) + +// supportedExts mirrors viper.SupportedExts. We hard-code the list rather +// than depend on viper here so this package can be used without pulling the +// configuration loader stack into tests. +var supportedExts = []string{ + "json", "toml", "yaml", "yml", "properties", "props", "prop", + "hcl", "tfvars", "dotenv", "env", "ini", +} + +// fileExists is overridable for tests. +var fileExists = func(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} + +// Detect returns the path to a CWD-resident grype config file if one would +// be picked up implicitly, otherwise it returns "". +// +// It returns "" (no warning) when: +// - the user passed an explicit -c / --config / --config=... flag +// - GRYPE_CONFIG (or the equivalent _CONFIG) is set +// - no recognised config file exists in the current working directory +// +// It looks for both finder layouts that fangs uses by default: +// - ./.. +// - ././config. +func Detect(appName string, args []string, env func(string) string) string { + if explicitConfigFlag(args) { + return "" + } + envKey := strings.ToUpper(appName) + "_CONFIG" + if env != nil && env(envKey) != "" { + return "" + } + + for _, ext := range supportedExts { + candidate := "." + appName + "." + ext + if fileExists(candidate) { + return candidate + } + } + subdir := "." + appName + for _, ext := range supportedExts { + candidate := filepath.Join(subdir, "config."+ext) + if fileExists(candidate) { + return candidate + } + } + return "" +} + +// explicitConfigFlag returns true if a -c / --config flag appears in args +// (in any of: "-c file", "--config file", "-c=file", "--config=file", or +// the bundled short form "-cfoo"). We intentionally accept false positives +// here over false negatives: if anything that looks like a config flag is +// present, suppress the warning. +func explicitConfigFlag(args []string) bool { + for i, a := range args { + switch { + case a == "-c", a == "--config": + if i+1 < len(args) { + return true + } + case strings.HasPrefix(a, "--config="): + return true + case strings.HasPrefix(a, "-c=") && len(a) > 3: + return true + case strings.HasPrefix(a, "-c") && len(a) > 2 && !strings.HasPrefix(a, "--"): + return true + } + } + return false +} diff --git a/cmd/grype/cli/internal/configwarn/configwarn_test.go b/cmd/grype/cli/internal/configwarn/configwarn_test.go new file mode 100644 index 00000000000..e8aca1f8983 --- /dev/null +++ b/cmd/grype/cli/internal/configwarn/configwarn_test.go @@ -0,0 +1,135 @@ +package configwarn + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDetect(t *testing.T) { + tests := []struct { + name string + app string + args []string + env map[string]string + present []string + want string + }{ + { + name: "no file present, no flag, no env", + app: "grype", + args: []string{"grype", "alpine:latest"}, + present: nil, + want: "", + }, + { + name: "cwd .grype.yaml present and no flag/env", + app: "grype", + args: []string{"grype", "alpine:latest"}, + present: []string{".grype.yaml"}, + want: ".grype.yaml", + }, + { + name: "cwd .grype.yml present and no flag/env", + app: "grype", + args: []string{"grype", "alpine:latest"}, + present: []string{".grype.yml"}, + want: ".grype.yml", + }, + { + name: "cwd .grype.json present and no flag/env", + app: "grype", + args: []string{"grype", "alpine:latest"}, + present: []string{".grype.json"}, + want: ".grype.json", + }, + { + name: "cwd .grype/config.yaml present and no flag/env", + app: "grype", + args: []string{"grype", "alpine:latest"}, + present: []string{filepath.Join(".grype", "config.yaml")}, + want: filepath.Join(".grype", "config.yaml"), + }, + { + name: "explicit --config suppresses warning", + app: "grype", + args: []string{"grype", "--config", "/etc/grype.yaml", "alpine:latest"}, + present: []string{".grype.yaml"}, + want: "", + }, + { + name: "explicit --config= suppresses warning", + app: "grype", + args: []string{"grype", "--config=/etc/grype.yaml", "alpine:latest"}, + present: []string{".grype.yaml"}, + want: "", + }, + { + name: "explicit -c suppresses warning", + app: "grype", + args: []string{"grype", "-c", "/etc/grype.yaml", "alpine:latest"}, + present: []string{".grype.yaml"}, + want: "", + }, + { + name: "explicit -c=path suppresses warning", + app: "grype", + args: []string{"grype", "-c=/etc/grype.yaml", "alpine:latest"}, + present: []string{".grype.yaml"}, + want: "", + }, + { + name: "explicit -cpath bundled short form suppresses warning", + app: "grype", + args: []string{"grype", "-c/etc/grype.yaml", "alpine:latest"}, + present: []string{".grype.yaml"}, + want: "", + }, + { + name: "GRYPE_CONFIG env suppresses warning", + app: "grype", + args: []string{"grype", "alpine:latest"}, + env: map[string]string{"GRYPE_CONFIG": "/etc/grype.yaml"}, + present: []string{".grype.yaml"}, + want: "", + }, + { + name: "trailing -c with no value is not treated as explicit", + app: "grype", + args: []string{"grype", "alpine:latest", "-c"}, + present: []string{".grype.yaml"}, + want: ".grype.yaml", + }, + { + name: "subdir form is only checked after the flat form", + app: "grype", + args: []string{"grype", "alpine:latest"}, + present: []string{".grype.yaml", filepath.Join(".grype", "config.yaml")}, + want: ".grype.yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + present := map[string]struct{}{} + for _, p := range tt.present { + present[p] = struct{}{} + } + origExists := fileExists + fileExists = func(path string) bool { + _, ok := present[path] + return ok + } + t.Cleanup(func() { fileExists = origExists }) + + env := func(string) string { return "" } + if tt.env != nil { + env = func(k string) string { return tt.env[k] } + } + + got := Detect(tt.app, tt.args, env) + assert.Equal(t, tt.want, got) + }) + } +}