Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,15 +588,15 @@ References -

### Kubernetes

To integrate Kubernetes, utilize the configuration block outlined below. This block allows you to specify Kubernetes connection details either through a file path or directly by providing the encoded kubeconfig content. If both kubeconfig_file and kubeconfig_encoded are specified, the kubeconfig_encoded will take precedence.
To integrate Kubernetes, utilize the configuration block outlined below. This block allows you to specify Kubernetes connection details either through a file path or directly by providing the encoded kubeconfig content. If both kubeconfig_file and kubeconfig_encoded are specified, the kubeconfig_encoded will take precedence. If neither is specified, the kubeconfig is resolved like kubectl: `$KUBECONFIG`, then `~/.kube/config`, then the in-cluster service account.


```yaml
- # This identifies the provider in use
provider: kubernetes
# User-defined identifier for filtering (optional).
id: staging
# Path to the kubeconfig file.
# Path to the kubeconfig file (optional).
kubeconfig_file: path/to/kubeconfig
# Base64 encoded kubeconfig, $ cat $KUBECONFIG | base64
kubeconfig_encoded: <encoded-kubeconfig>
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Flags:
CONFIGURATION:
-config string cloudlist flag config file (default "$HOME/.config/cloudlist/config.yaml")
-pc, -provider-config string provider config file (default "$HOME/.config/cloudlist/provider-config.yaml")
-ad, -auto-discovery discover providers from environment credentials (aws, gcp, azure, kubernetes) instead of the provider config

FILTERS:
-p, -provider value display results for given providers (comma-separated) (default linode,fastly,heroku,terraform,digitalocean,consul,cloudflare,hetzner,nomad,do,scw,openstack,alibaba,aws,gcp,namecheap,kubernetes,azure, custom)
Expand Down
43 changes: 43 additions & 0 deletions internal/runner/autodiscovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package runner

import (
"errors"

"github.com/projectdiscovery/cloudlist/pkg/inventory"
"github.com/projectdiscovery/cloudlist/pkg/schema"
"github.com/projectdiscovery/gologger"
)

// autoDiscoveryProviders resolve credentials without config through their SDK
// default chains: environment variables, CLI logins, instance metadata and
// in-cluster service accounts.
var autoDiscoveryProviders = []string{"aws", "gcp", "azure", "kubernetes"}

func autoDiscoveryConfig() schema.Options {
config := make(schema.Options, 0, len(autoDiscoveryProviders))
for _, provider := range autoDiscoveryProviders {
config = append(config, schema.OptionBlock{"provider": provider})
}
return config
}

func (r *Runner) newInventory(config schema.Options) (*inventory.Inventory, error) {
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.
discovered := &inventory.Inventory{}
for _, block := range config {
inv, err := inventory.New(schema.Options{block})
if err != nil {
gologger.Verbose().Msgf("Skipping %s: %s", block["provider"], err)
continue
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
discovered.Providers = append(discovered.Providers, inv.Providers...)
}
if len(discovered.Providers) == 0 {
return nil, errors.New("no provider credentials found in the environment")
}
return discovered, nil
}
48 changes: 48 additions & 0 deletions internal/runner/autodiscovery_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package runner

import (
"os"
"path/filepath"
"testing"

"github.com/projectdiscovery/cloudlist/pkg/schema"
"github.com/stretchr/testify/require"
)

func TestNewInventorySkipsProvidersWithoutCredentials(t *testing.T) {
kubeconfig := filepath.Join(t.TempDir(), "config")
require.NoError(t, os.WriteFile(kubeconfig, []byte(`apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
server: https://k8s.example.test:6443
contexts:
- name: test
context:
cluster: test
current-context: test
`), 0o600))
r := &Runner{options: &Options{AutoDiscovery: true}}

inv, err := r.newInventory(schema.Options{
{"provider": "kubernetes", "kubeconfig_file": filepath.Join(t.TempDir(), "missing")},
{"provider": "kubernetes", "kubeconfig_file": kubeconfig},
})
require.NoError(t, err)
require.Len(t, inv.Providers, 1)

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

func TestNewInventoryFailsFastWithoutAutoDiscovery(t *testing.T) {
r := &Runner{options: &Options{}}

_, err := r.newInventory(schema.Options{
{"provider": "kubernetes", "kubeconfig_file": filepath.Join(t.TempDir(), "missing")},
})
require.Error(t, err)
}
2 changes: 2 additions & 0 deletions internal/runner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Options struct {
ExtendedMetadata bool // ExtendedMetadata enables extended metadata for providers.
ProviderConfig string // ProviderConfig is the location of the provider config file.
DisableUpdateCheck bool // DisableUpdateCheck disable automatic update check
AutoDiscovery bool // AutoDiscovery discovers providers from environment credentials instead of the provider config.
}

var (
Expand Down Expand Up @@ -78,6 +79,7 @@ func ParseOptions() *Options {
flagSet.CreateGroup("config", "Configuration",
flagSet.StringVar(&options.Config, "config", defaultConfigLocation, "cloudlist flag config file"),
flagSet.StringVarP(&options.ProviderConfig, "provider-config", "pc", defaultProviderConfigLocation, "provider config file"),
flagSet.BoolVarP(&options.AutoDiscovery, "auto-discovery", "ad", false, "discover providers from environment credentials (aws, gcp, azure, kubernetes) instead of the provider config"),
)
flagSet.CreateGroup("filter", "Filters",
flagSet.StringSliceVarP(&options.Providers, "provider", "p", nil, "display results for given providers (comma-separated) (default "+strings.Join(defaultProviders, ",")+")", goflags.CommaSeparatedStringSliceOptions),
Expand Down
23 changes: 14 additions & 9 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"sync"

jsoniter "github.com/json-iterator/go"
"github.com/projectdiscovery/cloudlist/pkg/inventory"
"github.com/projectdiscovery/cloudlist/pkg/schema"
"github.com/projectdiscovery/cloudlist/pkg/schema/validate"
"github.com/projectdiscovery/gologger"
Expand All @@ -24,14 +23,20 @@ type Runner struct {
// New creates a new runner instance based on configuration options
func New(options *Options) (*Runner, error) {

if options.ProviderConfig == "" {
options.ProviderConfig = defaultProviderConfigLocation
gologger.Print().Msgf("Using default provider config: %s\n", options.ProviderConfig)
}
var config schema.Options
if options.AutoDiscovery {
config = autoDiscoveryConfig()
} else {
if options.ProviderConfig == "" {
options.ProviderConfig = defaultProviderConfigLocation
gologger.Print().Msgf("Using default provider config: %s\n", options.ProviderConfig)
}

config, err := readProviderConfig(options.ProviderConfig)
if err != nil {
return nil, err
var err error
config, err = readProviderConfig(options.ProviderConfig)
if err != nil {
return nil, err
}
}

// CLI overrides config
Expand Down Expand Up @@ -96,7 +101,7 @@ func (r *Runner) Enumerate() {
}
}

inventory, err := inventory.New(finalConfig)
inventory, err := r.newInventory(finalConfig)
if err != nil {
gologger.Fatal().Msgf("Could not create inventory: %s\n", err)
}
Expand Down
25 changes: 11 additions & 14 deletions pkg/providers/k8s/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,8 @@ const (
func New(options schema.OptionBlock) (*Provider, error) {
id, _ := options.GetMetadata("id")

configFile, ok := options.GetMetadata(kubeconfig_file)
configFile, _ := options.GetMetadata(kubeconfig_file)
configEncoded, strOk := options.GetMetadata(encodedKubeConfig)

if !ok && !strOk {
return nil, errkit.New("no kubeconfig_file or kubeconfig_encoded provided")
}
context, _ := options.GetMetadata("context")

var kubeConfig *rest.Config
Expand Down Expand Up @@ -111,19 +107,20 @@ func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) {
return finalList, nil
}

// buildConfigWithContext resolves the kubeconfig like kubectl when no path is
// given: $KUBECONFIG, then ~/.kube/config, then the in-cluster service account.
func buildConfigWithContext(context string, kubeconfigPath string) (*rest.Config, error) {
if context == "" {
kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
return kubeConfig, errkit.Wrap(err, "could not read kubeconfig file")
}
return kubeConfig, nil
}
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeconfigPath
Comment thread
coderabbitai[bot] marked this conversation as resolved.
kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{
CurrentContext: context,
}).ClientConfig()
if err != nil {
return nil, errkit.Wrap(err, "could not read kubeconfig file")
}
return kubeConfig, nil
}

func buildConfigFromStr(contextName string, config []byte) (*rest.Config, error) {
Expand Down
52 changes: 52 additions & 0 deletions pkg/providers/k8s/kubernetes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package k8s

import (
"os"
"path/filepath"
"testing"

"github.com/projectdiscovery/cloudlist/pkg/schema"
"github.com/stretchr/testify/require"
)

const testKubeconfig = `apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
server: https://k8s.example.test:6443
users:
- name: test
user:
token: test
contexts:
- name: test
context:
cluster: test
user: test
current-context: test
`

func isolateKubeconfig(t *testing.T, kubeconfig string) {
t.Helper()
t.Setenv("HOME", t.TempDir())
t.Setenv("KUBERNETES_SERVICE_HOST", "")
t.Setenv("KUBECONFIG", kubeconfig)
}

func TestNewWithoutKubeconfigUsesEnvironment(t *testing.T) {
path := filepath.Join(t.TempDir(), "config")
require.NoError(t, os.WriteFile(path, []byte(testKubeconfig), 0o600))
isolateKubeconfig(t, path)

p, err := New(schema.OptionBlock{})
require.NoError(t, err)
require.Equal(t, "k8s.example.test:6443", p.clientSet.CoreV1().RESTClient().Get().URL().Host)
}

func TestNewWithoutKubeconfigFailsWhenNoneFound(t *testing.T) {
isolateKubeconfig(t, filepath.Join(t.TempDir(), "missing"))

_, err := New(schema.OptionBlock{})
require.Error(t, err)
}
Loading