From cdfdda91a2bf4353a8e03f5efd74ad85efb42dc1 Mon Sep 17 00:00:00 2001 From: vjeffrey Date: Wed, 15 Feb 2023 17:55:56 -0700 Subject: [PATCH] cnquery can read config file from aws ssm parameter store (#941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` [15/02/23 12:27:02] ❯ go run apps/cnspec/cnspec.go scan aws --config aws-ssm-ps://region/us-east-1/parameter/MondooAgentConfig → loaded configuration from aws-ssm-ps://region/us-east-1/parameter/MondooAgentConfig using source --config → using service account credentials ``` cnspec can read the service account credentials stored in AWS ssm parameter store --- cli/config/aws-ssm-ps.go | 109 ++++++++++++++++++++++++++++++++++ cli/config/aws_ssm_ps_test.go | 41 +++++++++++++ cli/config/config.go | 9 +++ 3 files changed, 159 insertions(+) create mode 100644 cli/config/aws-ssm-ps.go create mode 100644 cli/config/aws_ssm_ps_test.go diff --git a/cli/config/aws-ssm-ps.go b/cli/config/aws-ssm-ps.go new file mode 100644 index 0000000000..ef0245d365 --- /dev/null +++ b/cli/config/aws-ssm-ps.go @@ -0,0 +1,109 @@ +package config + +import ( + "context" + "io" + "path" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +const AWS_SSM_PARAMETERSTORE_PREFIX = "aws-ssm-ps://" + +// loads the configuration from aws ssm parameter store +func loadAwsSSMParameterStore(key string) error { + viper.RemoteConfig = &awsSSMParamConfigFactory{} + viper.SupportedRemoteProviders = []string{"aws-ssm-ps"} + ssmKey := strings.TrimPrefix(key, AWS_SSM_PARAMETERSTORE_PREFIX) + log.Info().Str("key", ssmKey).Msg("look for configuration stored in aws ssm parameter store") + err := viper.AddRemoteProvider("aws-ssm-ps", "localhost", ssmKey) + if err != nil { + return errors.Wrap(err, "could not initialize gs provider") + } + viper.SetConfigType("yaml") + err = viper.ReadRemoteConfig() + if err != nil { + return errors.Wrapf(err, "could not read aws ssm parameter config from %s", ssmKey) + } + + return nil +} + +type awsSSMParamConfigFactory struct{} + +func (a *awsSSMParamConfigFactory) Get(rp viper.RemoteProvider) (io.Reader, error) { + ssmParameter, err := ParseSSMParameterPath(rp.Path()) + if err != nil { + return nil, err + } + ctx := context.Background() + + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, err + } + + cfg.Region = ssmParameter.Region + ps := ssm.NewFromConfig(cfg) + + out, err := ps.GetParameter(ctx, &ssm.GetParameterInput{ + Name: aws.String(ssmParameter.Parameter), + WithDecryption: aws.Bool(true), // this field is ignored if the parameter is a string or stringlist, so it's ok to have it on by default + }) + if err != nil { + return nil, err + } + + return strings.NewReader(*out.Parameter.Value), nil +} + +func (g *awsSSMParamConfigFactory) Watch(rp viper.RemoteProvider) (io.Reader, error) { + return nil, errors.New("not implemented") +} + +func (g *awsSSMParamConfigFactory) WatchChannel(rp viper.RemoteProvider) (<-chan *viper.RemoteResponse, chan bool) { + return nil, nil +} + +type SsmParameter struct { + Parameter string + Region string + // todo: add optional decrypt and account arguments +} + +func NewSSMParameter(region string, parameter string) (*SsmParameter, error) { + if region == "" || parameter == "" { + return nil, errors.New("invalid parameter. region and parameter name required.") + } + return &SsmParameter{Region: region, Parameter: parameter}, nil +} + +func (s *SsmParameter) String() string { + // e.g. region/us-east-2/parameter/MondooAgentConfig + return path.Join("region", s.Region, "parameter", s.Parameter) +} + +func ParseSSMParameterPath(path string) (*SsmParameter, error) { + if !IsValidSSMParameterPath(path) { + return nil, errors.New("invalid parameter path. expected region//parameter/") + } + keyValues := strings.Split(path, "/") + if len(keyValues) != 4 { + return nil, errors.New("invalid parameter path. expected region//parameter/") + } + return NewSSMParameter(keyValues[1], keyValues[3]) +} + +var VALID_SSM_PARAMETER_PATH = regexp.MustCompile(`^region\/(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d\/parameter\/.+$`) + +func IsValidSSMParameterPath(path string) bool { + return VALID_SSM_PARAMETER_PATH.MatchString(path) +} diff --git a/cli/config/aws_ssm_ps_test.go b/cli/config/aws_ssm_ps_test.go new file mode 100644 index 0000000000..8b3f97e2d3 --- /dev/null +++ b/cli/config/aws_ssm_ps_test.go @@ -0,0 +1,41 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// aws-ssm-ps://region/us-east-2/parameter/MondooAgentConfig?decrypt=true&account=12345678 + +func TestNewSSMParameter(t *testing.T) { + param, err := NewSSMParameter("us-west-1", "test-name") + require.NoError(t, err) + assert.Equal(t, &SsmParameter{Region: "us-west-1", Parameter: "test-name"}, param) + assert.Equal(t, "region/us-west-1/parameter/test-name", param.String()) +} + +func TestParseSSMParameterPath(t *testing.T) { + ssmParam, err := ParseSSMParameterPath("region/us-west-2/parameter/test-param-name") + require.NoError(t, err) + assert.Equal(t, &SsmParameter{Parameter: "test-param-name", Region: "us-west-2"}, ssmParam) +} + +func TestNewSSMParameterPathReturnsErrWhenNoRegion(t *testing.T) { + _, err := NewSSMParameter("", "test-name") + require.Error(t, err) + assert.EqualError(t, err, "invalid parameter. region and parameter name required.") +} + +func TestParseSSMParameterPathBadPathReturnsError(t *testing.T) { + _, err := ParseSSMParameterPath("region/us-west-2/parameter") + require.Error(t, err) + assert.EqualError(t, err, "invalid parameter path. expected region//parameter/") + _, err = ParseSSMParameterPath("region//parameter/testname") + require.Error(t, err) + assert.EqualError(t, err, "invalid parameter path. expected region//parameter/") + _, err = ParseSSMParameterPath("region/us-west-1/parameter/") + require.Error(t, err) + assert.EqualError(t, err, "invalid parameter path. expected region//parameter/") +} diff --git a/cli/config/config.go b/cli/config/config.go index 7964156118..834d445d9e 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -173,6 +173,15 @@ func initConfig() { } else { Source = "default" } + if strings.HasPrefix(Path, AWS_SSM_PARAMETERSTORE_PREFIX) { + err := loadAwsSSMParameterStore(Path) + if err != nil { + LoadedConfig = false + log.Error().Err(err).Str("path", Path).Msg("could not load aws parameter store config") + } else { + LoadedConfig = true + } + } // check if the default config file is available if Path == "" && Source != configSourceBase64 {