Skip to content

Commit

Permalink
⭐ cnquery can read config file from aws ssm parameter store
Browse files Browse the repository at this point in the history
  • Loading branch information
vjeffrey committed Feb 15, 2023
1 parent c71e44f commit d6b1eb5
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 0 deletions.
109 changes: 109 additions & 0 deletions cli/config/aws-ssm-ps.go
Original file line number Diff line number Diff line change
@@ -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/<region-val>/parameter/<parameter-name>")
}
keyValues := strings.Split(path, "/")
if len(keyValues) != 4 {
return nil, errors.New("invalid parameter path. expected region/<region-val>/parameter/<parameter-name>")
}
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)
}
41 changes: 41 additions & 0 deletions cli/config/aws_ssm_ps_test.go
Original file line number Diff line number Diff line change
@@ -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/<region-val>/parameter/<parameter-name>")
_, err = ParseSSMParameterPath("region//parameter/testname")
require.Error(t, err)
assert.EqualError(t, err, "invalid parameter path. expected region/<region-val>/parameter/<parameter-name>")
_, err = ParseSSMParameterPath("region/us-west-1/parameter/")
require.Error(t, err)
assert.EqualError(t, err, "invalid parameter path. expected region/<region-val>/parameter/<parameter-name>")
}
15 changes: 15 additions & 0 deletions cli/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ func InventoryPath(configPath string) (string, bool) {
return inventoryPath, probeConfig(inventoryPath)
}

func LoadConfig() {
initConfig()
}

func initConfig() {
viper.SetConfigType("yaml")

Expand All @@ -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 {
Expand Down

0 comments on commit d6b1eb5

Please sign in to comment.