From 1e9e315fb23feab4f4a91e0e9e6da07378ebf539 Mon Sep 17 00:00:00 2001 From: Jonathan Davies Date: Wed, 10 Sep 2025 17:27:30 +0100 Subject: [PATCH 1/2] feat: changes for assuming a role and passing creds in config --- README.md | 30 ++++++++++++++++++++++----- go.mod | 12 +++++------ go.sum | 12 +++++++++++ main.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7a6774d..0fc1651 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,36 @@ To authenticate this plugin, you must provide AWS credentials in one of the foll ## Configuration +Configuration must contain an AWS account ID. Additionally, role credentials can be passed through the configuration, otherwise they will attempt to be found from the environment. Optionally, you can also define a role ARN to assume which will be used to retrieve the relevant budget data. + ```yaml plugins: aws_budget: - # Token for user with access to costs & billing API - access-key-id: "" - secret-access-key: "" - # additionaly define a session-token for an assumed role: - session-token: "" # ID of the AWS Account that you want to check budgets for account-id: 123456789012 + # (Optional) Federated credentials to use + access-key-id: "..." + secret-access-key: "..." + session-token: "..." + # (Optional) Role to assume for retrieving budgets data + assume-role-arn: "arn:aws:iam::123456789012:role/example-role" +``` + +If a role is defined, it must have the following policy statement as a minimum: +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSBudgetsPermissions", + "Effect": "Allow", + "Action": [ + "budgets:ViewBudget" + ], + "Resource": "arn:aws:budgets:::budget/*" + } + ] +} ``` ## Integration testing diff --git a/go.mod b/go.mod index 37fdb55..ad88ea1 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/compliance-framework/plugin-aws-budget go 1.23.2 require ( - github.com/aws/aws-sdk-go-v2 v1.38.3 + github.com/aws/aws-sdk-go-v2 v1.39.0 github.com/aws/aws-sdk-go-v2/config v1.29.9 github.com/aws/aws-sdk-go-v2/service/budgets v1.37.3 + github.com/aws/aws-sdk-go-v2/service/sts v1.38.3 github.com/compliance-framework/agent v0.2.1 github.com/google/go-github/v71 v71.0.0 github.com/hashicorp/go-hclog v1.5.0 @@ -18,14 +19,13 @@ require ( github.com/agnivade/levenshtein v1.2.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect github.com/aws/smithy-go v1.23.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index 42813b6..dc1a96e 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38y github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk= github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4= +github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= @@ -16,10 +18,14 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/service/billing v1.7.3 h1:7AeVkpSsE1wOaopMkjHRC3AGjT4bxPbwgclqpSkoQ0Y= @@ -30,14 +36,20 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0 h1:qzT4wyLo7ssa4QU8Xcf+h+iyCF4 github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0/go.mod h1:ouvGEfHbLaIlWwpDpOVWPWR+YwO0HDv3vm5tYLq8ImY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA= github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.3 h1:yEiZ0ztgji2GsCb/6uQSITXcGdtmWMfLRys0jJFiUkc= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.3/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= diff --git a/main.go b/main.go index 66b1fc3..5ec309c 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,10 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/budgets" "github.com/aws/aws-sdk-go-v2/service/budgets/types" + "github.com/aws/aws-sdk-go-v2/service/sts" policyManager "github.com/compliance-framework/agent/policy-manager" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" @@ -35,6 +37,11 @@ type Validator interface { type PluginConfig struct { AccountId string `mapstructure:"account_id"` + AwsAccessKeyId string `mapstructure:"aws_access_key_id"` + AwsSecretAccessKey string `mapstructure:"aws_secret_access_key"` + AwsSessionToken string `mapstructure:"aws_session_token"` + AssumeRoleArn string `mapstructure:"assume_role_arn"` + } func (c *PluginConfig) Validate() error { @@ -44,6 +51,53 @@ func (c *PluginConfig) Validate() error { return nil } +// TODO: move to shared lib +func loadAWSConfig(ctx context.Context, pluginConfig *PluginConfig) (*aws.Config, error) { + var awsConfig aws.Config + var err error + + if pluginConfig.AwsAccessKeyId != "" && pluginConfig.AwsSecretAccessKey != "" && pluginConfig.AwsSessionToken != "" { + // Use credentials if in config + creds := aws.NewCredentialsCache( + credentials.NewStaticCredentialsProvider( + pluginConfig.AwsAccessKeyId, + pluginConfig.AwsSecretAccessKey, + pluginConfig.AwsSessionToken, + ), + ) + awsConfig, err = config.LoadDefaultConfig(ctx, config.WithRegion(os.Getenv("AWS_REGION")), config.WithCredentialsProvider(creds)) + } else { + // Otherwise resort to env + awsConfig, err = config.LoadDefaultConfig(ctx, config.WithRegion(os.Getenv("AWS_REGION"))) + } + if err != nil { + return nil, err + } + if pluginConfig.AssumeRoleArn == "" { + return &awsConfig, nil + } + + // If given a role to assume, assume it + stsClient := sts.NewFromConfig(awsConfig) + sessionName := "plugin-aws-budget" + assumeRoleOutput, err := stsClient.AssumeRole(ctx, &sts.AssumeRoleInput{RoleArn: &pluginConfig.AssumeRoleArn, RoleSessionName: &sessionName}) + if err != nil { + return nil, err + } + creds := aws.NewCredentialsCache( + credentials.NewStaticCredentialsProvider( + *assumeRoleOutput.Credentials.AccessKeyId, + *assumeRoleOutput.Credentials.SecretAccessKey, + *assumeRoleOutput.Credentials.SessionToken, + ), + ) + awsConfig, err = config.LoadDefaultConfig(ctx, config.WithRegion(os.Getenv("AWS_REGION")), config.WithCredentialsProvider(creds)) + if err != nil { + return nil, err + } + return &awsConfig, nil +} + func (l *AWSBudgetPlugin) Configure(req *proto.ConfigureRequest) (*proto.ConfigureResponse, error) { l.Logger.Info("Configuring AWS Budget Plugin") @@ -59,14 +113,14 @@ func (l *AWSBudgetPlugin) Configure(req *proto.ConfigureRequest) (*proto.Configu return nil, err } - awsConfig, err := config.LoadDefaultConfig(ctx, config.WithRegion(os.Getenv("AWS_REGION"))) + awsConfig, err := loadAWSConfig(ctx, pluginConfig) if err != nil { - l.Logger.Error("unable to load SDK config", "error", err) + l.Logger.Error("Error loading AWS config!", "error", err) return nil, err } l.config = pluginConfig - l.awsBudgetClient = budgets.NewFromConfig(awsConfig) + l.awsBudgetClient = budgets.NewFromConfig(*awsConfig) return &proto.ConfigureResponse{}, nil } @@ -222,6 +276,7 @@ func (l *AWSBudgetPlugin) Eval(request *proto.EvalRequest, apiHelper runner.ApiH activities, ) evidence, err := processor.GenerateResults(ctx, policyPath, budgetMap) + l.Logger.Info(fmt.Sprintf("evidence: %v", evidence)) evidences = slices.Concat(evidences, evidence) if err != nil { accumulatedErrors = errors.Join(accumulatedErrors, err) From ead68338fbaaa633dc9be633edca878f472122bb Mon Sep 17 00:00:00 2001 From: Jonathan Davies Date: Wed, 10 Sep 2025 17:30:20 +0100 Subject: [PATCH 2/2] fix: logs --- main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.go b/main.go index 5ec309c..a7743d5 100644 --- a/main.go +++ b/main.go @@ -115,7 +115,7 @@ func (l *AWSBudgetPlugin) Configure(req *proto.ConfigureRequest) (*proto.Configu awsConfig, err := loadAWSConfig(ctx, pluginConfig) if err != nil { - l.Logger.Error("Error loading AWS config!", "error", err) + l.Logger.Error("Error loading AWS config", "error", err) return nil, err } @@ -276,7 +276,6 @@ func (l *AWSBudgetPlugin) Eval(request *proto.EvalRequest, apiHelper runner.ApiH activities, ) evidence, err := processor.GenerateResults(ctx, policyPath, budgetMap) - l.Logger.Info(fmt.Sprintf("evidence: %v", evidence)) evidences = slices.Concat(evidences, evidence) if err != nil { accumulatedErrors = errors.Join(accumulatedErrors, err)