From d6b1eb5aff0dbf3d4ec624d2d5ca329f88d9748e Mon Sep 17 00:00:00 2001 From: Victoria Jeffrey Date: Wed, 15 Feb 2023 12:25:32 -0700 Subject: [PATCH] =?UTF-8?q?=E2=AD=90=20cnquery=20can=20read=20config=20fil?= =?UTF-8?q?e=20from=20aws=20ssm=20parameter=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/config/aws-ssm-ps.go | 109 ++++++++++++++++++++++++++++++++++ cli/config/aws_ssm_ps_test.go | 41 +++++++++++++ cli/config/config.go | 15 +++++ 3 files changed, 165 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..60d66285b9 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -152,6 +152,10 @@ func InventoryPath(configPath string) (string, bool) { return inventoryPath, probeConfig(inventoryPath) } +func LoadConfig() { + initConfig() +} + func initConfig() { viper.SetConfigType("yaml") @@ -173,6 +177,17 @@ func initConfig() { } else { Source = "default" } + if len(Path) > 0 { + 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 {