Skip to content

Commit

Permalink
Prepare Cloudbeat for AWS Gov cloud (#2050)
Browse files Browse the repository at this point in the history
* add regional fallback for AWS Gov
* make S3 fetcher partition agnostic
* make CloudTrail fetcher partition agnostic
* handle IAM credentials report better
* fix region picker for AWS Organizations
  • Loading branch information
kubasobon committed Mar 26, 2024
1 parent 6db9c91 commit 9e7920d
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 35 deletions.
36 changes: 29 additions & 7 deletions internal/flavors/benchmark/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"errors"
"fmt"

awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/elastic/beats/v7/x-pack/libbeat/common/aws"
"github.com/elastic/elastic-agent-libs/logp"

Expand Down Expand Up @@ -59,23 +60,44 @@ func (a *AWS) initialize(ctx context.Context, log *logp.Logger, cfg *config.Conf
return nil, nil, nil, err
}

// TODO: make this mock-able
awsConfig, err := aws.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err)
var (
awsConfig *awssdk.Config
awsIdentity *cloud.Identity
err error
)

awsConfig, awsIdentity, err = a.getIdentity(ctx, cfg)
if err != nil && cfg.CloudConfig.Aws.Cred.DefaultRegion == "" {
log.Warn("failed to initialize identity; retrying to check AWS Gov Cloud regions")
cfg.CloudConfig.Aws.Cred.DefaultRegion = awslib.DefaultGovRegion
awsConfig, awsIdentity, err = a.getIdentity(ctx, cfg)
}

awsIdentity, err := a.IdentityProvider.GetIdentity(ctx, awsConfig)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get AWS identity: %w", err)
return nil, nil, nil, fmt.Errorf("failed to get AWS Identity: %w", err)
}
log.Info("successfully retrieved AWS Identity")

return registry.NewRegistry(
log,
registry.WithFetchersMap(preset.NewCisAwsFetchers(log, awsConfig, ch, awsIdentity)),
registry.WithFetchersMap(preset.NewCisAwsFetchers(log, *awsConfig, ch, awsIdentity)),
), cloud.NewDataProvider(cloud.WithAccount(*awsIdentity)), nil, nil
}

func (a *AWS) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) {
awsConfig, err := aws.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err)
}

awsIdentity, err := a.IdentityProvider.GetIdentity(ctx, awsConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to get AWS identity: %w", err)
}

return &awsConfig, awsIdentity, nil
}

func (a *AWS) checkDependencies() error {
if a.IdentityProvider == nil {
return errors.New("aws identity provider is uninitialized")
Expand Down
39 changes: 30 additions & 9 deletions internal/flavors/benchmark/aws_org.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,30 @@ func (a *AWSOrg) initialize(ctx context.Context, log *logp.Logger, cfg *config.C
return nil, nil, nil, err
}

// TODO: make this mock-able
awsConfig, err := aws.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err)
}
var (
awsConfig *awssdk.Config
awsIdentity *cloud.Identity
err error
)

a.IAMProvider = iam.NewIAMProvider(log, awsConfig, nil)
awsConfig, awsIdentity, err = a.getIdentity(ctx, cfg)
if err != nil && cfg.CloudConfig.Aws.Cred.DefaultRegion == "" {
log.Warn("failed to initialize identity; retrying to check AWS Gov Cloud regions")
cfg.CloudConfig.Aws.Cred.DefaultRegion = awslib.DefaultGovRegion
awsConfig, awsIdentity, err = a.getIdentity(ctx, cfg)
}

awsIdentity, err := a.IdentityProvider.GetIdentity(ctx, awsConfig)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get AWS identity: %w", err)
return nil, nil, nil, fmt.Errorf("failed to get AWS Identity: %w", err)
}
log.Info("successfully retrieved AWS Identity")

a.IAMProvider = iam.NewIAMProvider(log, *awsConfig, nil)

cache := make(map[string]registry.FetchersMap)
reg := registry.NewRegistry(log, registry.WithUpdater(
func() (registry.FetchersMap, error) {
accounts, err := a.getAwsAccounts(ctx, log, awsConfig, awsIdentity)
accounts, err := a.getAwsAccounts(ctx, log, *awsConfig, awsIdentity)
if err != nil {
return nil, fmt.Errorf("failed to get AWS accounts: %w", err)
}
Expand Down Expand Up @@ -211,6 +218,20 @@ func (a *AWSOrg) pickManagementAccountRole(ctx context.Context, log *logp.Logger
return config, nil
}

func (a *AWSOrg) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) {
awsConfig, err := aws.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err)
}

awsIdentity, err := a.IdentityProvider.GetIdentity(ctx, awsConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to get AWS identity: %w", err)
}

return &awsConfig, awsIdentity, nil
}

func (a *AWSOrg) checkDependencies() error {
if a.IAMProvider == nil {
return errors.New("aws iam provider is uninitialized")
Expand Down
34 changes: 24 additions & 10 deletions internal/resources/providers/awslib/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ package awslib

import (
"errors"

"github.com/elastic/cloudbeat/internal/resources/utils/pointers"
)

const (
DefaultRegion = "us-east-1"
GlobalRegion = "global"
DefaultRegion = "us-east-1"
DefaultGovRegion = "us-gov-east-1"
GlobalRegion = "global"
)

var ErrClientNotFound = errors.New("aws client not found")
Expand All @@ -35,18 +38,29 @@ type AwsResource interface {
GetRegion() string
}

func GetClient[T any](region *string, list map[string]T) (T, error) {
c, ok := list[getRegion(region)]
if !ok {
return c, ErrClientNotFound
func GetDefaultClient[T any](list map[string]T) (T, error) {
c, ok := list[DefaultRegion]
if ok {
return c, nil
}
return c, nil

c, ok = list[DefaultGovRegion]
if ok {
return c, nil
}

return c, ErrClientNotFound
}

func getRegion(region *string) string {
func GetClient[T any](region *string, list map[string]T) (T, error) {
if region == nil {
return DefaultRegion
return GetDefaultClient(list)
}

return *region
c, ok := list[pointers.Deref(region)]
if !ok {
return c, ErrClientNotFound
}

return c, nil
}
7 changes: 6 additions & 1 deletion internal/resources/providers/awslib/cloudtrail/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cloudtrail

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/service/cloudtrail"
"github.com/aws/aws-sdk-go-v2/service/cloudtrail/types"
Expand All @@ -40,7 +41,11 @@ type Client interface {

func (p Provider) DescribeTrails(ctx context.Context) ([]TrailInfo, error) {
input := cloudtrail.DescribeTrailsInput{}
output, err := p.clients[awslib.DefaultRegion].DescribeTrails(ctx, &input)
defaultClient, err := awslib.GetDefaultClient(p.clients)
if err != nil {
return nil, fmt.Errorf("could not select default region client: %w", err)
}
output, err := defaultClient.DescribeTrails(ctx, &input)
if err != nil {
return nil, err
}
Expand Down
13 changes: 9 additions & 4 deletions internal/resources/providers/awslib/iam/root_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ func (p Provider) getRootAccountUser(rootAccount *CredentialReport) *types.User
return nil
}

pwdLastUsed, err := time.Parse(time.RFC3339, rootAccount.PasswordLastUsed)
if err != nil {
p.log.Errorf("fail to parse root account password last used, error: %v", err)
return nil
pwdLastUsed := time.Time{}
// "no_information" if never used, "N/A" if user has no password
// Docs: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html
if rootAccount.PasswordLastUsed != "no_information" && rootAccount.PasswordLastUsed != "N/A" {
pwdLastUsed, err = time.Parse(time.RFC3339, rootAccount.PasswordLastUsed)
if err != nil {
p.log.Errorf("fail to parse root account password last used, error: %v", err)
return nil
}
}

return &types.User{
Expand Down
20 changes: 16 additions & 4 deletions internal/resources/providers/awslib/s3/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ func NewProvider(log *logp.Logger, cfg aws.Config, factory awslib.CrossRegionFac
}

func (p Provider) DescribeBuckets(ctx context.Context) ([]awslib.AwsResource, error) {
clientBuckets, err := p.clients[awslib.DefaultRegion].ListBuckets(ctx, &s3Client.ListBucketsInput{})
defaultClient, err := awslib.GetDefaultClient(p.clients)
if err != nil {
return nil, fmt.Errorf("could not select default region client: %w", err)
}
clientBuckets, err := defaultClient.ListBuckets(ctx, &s3Client.ListBucketsInput{})
if err != nil {
p.log.Errorf("Could not list s3 buckets: %v", err)
return nil, err
Expand Down Expand Up @@ -225,15 +229,23 @@ func (p Provider) getBucketEncryptionAlgorithm(ctx context.Context, bucketName *
}

func (p Provider) getBucketRegion(ctx context.Context, bucketName *string) (string, error) {
location, err := p.clients[awslib.DefaultRegion].GetBucketLocation(ctx, &s3Client.GetBucketLocationInput{Bucket: bucketName})
defaultClient, err := awslib.GetDefaultClient(p.clients)
if err != nil {
return "", fmt.Errorf("could not select default region client: %w", err)
}
location, err := defaultClient.GetBucketLocation(ctx, &s3Client.GetBucketLocationInput{Bucket: bucketName})
if err != nil {
return "", err
}

region := string(location.LocationConstraint)
// Region us-east-1 have a LocationConstraint of null.
// Region us-east-1 have a LocationConstraint of null...
if region == "" {
region = "us-east-1"
region = awslib.DefaultRegion
// ...but check if it's not the AWS GovCloud partition
if _, ok := p.clients[awslib.DefaultRegion]; !ok {
region = awslib.DefaultGovRegion
}
}

return region, nil
Expand Down

0 comments on commit 9e7920d

Please sign in to comment.