diff --git a/internal/runner/autodiscovery.go b/internal/runner/autodiscovery.go index 0cf481ec..57765d07 100644 --- a/internal/runner/autodiscovery.go +++ b/internal/runner/autodiscovery.go @@ -2,6 +2,8 @@ package runner import ( "errors" + "fmt" + "strings" "github.com/projectdiscovery/cloudlist/pkg/inventory" "github.com/projectdiscovery/cloudlist/pkg/schema" @@ -9,8 +11,7 @@ import ( ) // autoDiscoveryProviders resolve credentials without config through their SDK -// default chains: environment variables, CLI logins, instance metadata and -// in-cluster service accounts. +// default credential chains. var autoDiscoveryProviders = []string{"aws", "gcp", "azure", "kubernetes"} func autoDiscoveryConfig() schema.Options { @@ -25,19 +26,27 @@ func (r *Runner) newInventory(config schema.Options) (*inventory.Inventory, erro if !r.options.AutoDiscovery { return inventory.New(config) } - // A provider fails to initialize when the environment has no credentials - // for it, which is expected here, so skip it instead of aborting. + if len(config) == 0 { + return nil, fmt.Errorf("no auto-discovery provider matches the -p/-id filters, supported: %s", strings.Join(autoDiscoveryProviders, ",")) + } + // Init fails for missing credentials but also for denied, expired or + // unreachable ones, so skip failed providers and report why. discovered := &inventory.Inventory{} + var errs []error for _, block := range config { inv, err := inventory.New(schema.Options{block}) if err != nil { - gologger.Verbose().Msgf("Skipping %s: %s", block["provider"], err) + errs = append(errs, err) continue } discovered.Providers = append(discovered.Providers, inv.Providers...) } if len(discovered.Providers) == 0 { - return nil, errors.New("no provider credentials found in the environment") + return nil, fmt.Errorf("no provider could be initialized: %w", errors.Join(errs...)) + } + for _, err := range errs { + // Info stays visible at the default log level. Warning is dropped unless -v is set. + gologger.Info().Msgf("Skipping provider: %s", err) } return discovered, nil } diff --git a/internal/runner/autodiscovery_test.go b/internal/runner/autodiscovery_test.go index d2b78444..8587c281 100644 --- a/internal/runner/autodiscovery_test.go +++ b/internal/runner/autodiscovery_test.go @@ -3,13 +3,52 @@ package runner import ( "os" "path/filepath" + "strings" "testing" "github.com/projectdiscovery/cloudlist/pkg/schema" + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/gologger/levels" + "github.com/projectdiscovery/gologger/writer" "github.com/stretchr/testify/require" ) -func TestNewInventorySkipsProvidersWithoutCredentials(t *testing.T) { +type logRecord struct { + message string + level levels.Level +} + +type captureWriter struct { + records []logRecord +} + +func (w *captureWriter) Write(data []byte, level levels.Level) { + w.records = append(w.records, logRecord{message: string(data), level: level}) +} + +func captureDefaultLog(t *testing.T) *captureWriter { + t.Helper() + captured := &captureWriter{} + gologger.DefaultLogger.SetMaxLevel(levels.LevelInfo) + gologger.DefaultLogger.SetWriter(captured) + t.Cleanup(func() { + gologger.DefaultLogger.SetMaxLevel(levels.LevelInfo) + gologger.DefaultLogger.SetWriter(writer.NewCLI()) + }) + return captured +} + +func skipMessage(records []logRecord) string { + for _, rec := range records { + if rec.level == levels.LevelInfo && strings.Contains(rec.message, "Skipping provider") { + return rec.message + } + } + return "" +} + +func writeKubeconfig(t *testing.T) string { + t.Helper() kubeconfig := filepath.Join(t.TempDir(), "config") require.NoError(t, os.WriteFile(kubeconfig, []byte(`apiVersion: v1 kind: Config @@ -23,7 +62,13 @@ contexts: cluster: test current-context: test `), 0o600)) + return kubeconfig +} + +func TestNewInventorySkipsProvidersWithoutCredentials(t *testing.T) { + kubeconfig := writeKubeconfig(t) r := &Runner{options: &Options{AutoDiscovery: true}} + logs := captureDefaultLog(t) inv, err := r.newInventory(schema.Options{ {"provider": "kubernetes", "kubeconfig_file": filepath.Join(t.TempDir(), "missing")}, @@ -31,11 +76,23 @@ current-context: test }) require.NoError(t, err) require.Len(t, inv.Providers, 1) + skip := skipMessage(logs.records) + require.Contains(t, skip, "could not create provider kubernetes") + logs.records = nil _, err = r.newInventory(schema.Options{ {"provider": "kubernetes", "kubeconfig_file": filepath.Join(t.TempDir(), "missing")}, }) - require.EqualError(t, err, "no provider credentials found in the environment") + require.ErrorContains(t, err, "no provider could be initialized") + require.ErrorContains(t, err, "could not create provider kubernetes") + require.Empty(t, skipMessage(logs.records)) +} + +func TestNewInventoryWithoutMatchingProviders(t *testing.T) { + r := &Runner{options: &Options{AutoDiscovery: true}} + + _, err := r.newInventory(schema.Options{}) + require.ErrorContains(t, err, "no auto-discovery provider matches the -p/-id filters") } func TestNewInventoryFailsFastWithoutAutoDiscovery(t *testing.T) { @@ -43,6 +100,7 @@ func TestNewInventoryFailsFastWithoutAutoDiscovery(t *testing.T) { _, err := r.newInventory(schema.Options{ {"provider": "kubernetes", "kubeconfig_file": filepath.Join(t.TempDir(), "missing")}, + {"provider": "kubernetes", "kubeconfig_file": writeKubeconfig(t)}, }) - require.Error(t, err) + require.ErrorContains(t, err, "could not create provider kubernetes") } diff --git a/internal/runner/options.go b/internal/runner/options.go index 3d93efeb..d3887d68 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -123,7 +123,9 @@ func ParseOptions() *Options { } } - checkAndCreateProviderConfigFile(options) + if !options.AutoDiscovery { + checkAndCreateProviderConfigFile(options) + } return options } diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 463e1c34..6eac0ebc 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -1,9 +1,11 @@ package runner import ( + "path/filepath" "testing" "github.com/projectdiscovery/cloudlist/pkg/schema" + "github.com/stretchr/testify/require" ) func TestSanitizePrivateIPs(t *testing.T) { @@ -56,3 +58,14 @@ func TestSanitizePrivateIPsOnMisclassifiedFields(t *testing.T) { t.Fatalf("expected public addresses to remain when exclusion enabled") } } + +func TestNewWithAutoDiscoveryIgnoresProviderConfig(t *testing.T) { + r, err := New(&Options{AutoDiscovery: true, ProviderConfig: filepath.Join(t.TempDir(), "missing.yaml")}) + require.NoError(t, err) + + var providers []string + for _, block := range r.config { + providers = append(providers, block["provider"]) + } + require.Equal(t, autoDiscoveryProviders, providers) +}