internal/flavors/benchmark/aws_org.go (182 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package benchmark import ( "context" "errors" "fmt" awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/elastic/cloudbeat/internal/config" "github.com/elastic/cloudbeat/internal/dataprovider" "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/flavors/benchmark/builder" "github.com/elastic/cloudbeat/internal/infra/clog" "github.com/elastic/cloudbeat/internal/resources/fetching" "github.com/elastic/cloudbeat/internal/resources/fetching/preset" "github.com/elastic/cloudbeat/internal/resources/fetching/registry" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" "github.com/elastic/cloudbeat/internal/resources/utils/pointers" ) const ( rootRole = "cloudbeat-root" memberRole = "cloudbeat-securityaudit" scanSettingTagKey = "cloudbeat_scan_management_account" scanSettingTagValue = "Yes" ) type AWSOrg struct { IAMProvider iam.RoleGetter IdentityProvider awslib.IdentityProviderGetter AccountProvider awslib.AccountProviderAPI } func (a *AWSOrg) NewBenchmark(ctx context.Context, log *clog.Logger, cfg *config.Config) (builder.Benchmark, error) { resourceCh := make(chan fetching.ResourceInfo, resourceChBufferSize) reg, bdp, _, err := a.initialize(ctx, log, cfg, resourceCh) if err != nil { return nil, err } return builder.New( builder.WithBenchmarkDataProvider(bdp), ).Build(ctx, log, cfg, resourceCh, reg) } //revive:disable-next-line:function-result-limit func (a *AWSOrg) initialize(ctx context.Context, log *clog.Logger, cfg *config.Config, ch chan fetching.ResourceInfo) (registry.Registry, dataprovider.CommonDataProvider, dataprovider.IdProvider, error) { if err := a.checkDependencies(); err != nil { return nil, nil, nil, 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) } if err != nil { return nil, nil, nil, fmt.Errorf("failed to get AWS Identity: %w", err) } log.Info("successfully retrieved AWS Identity") a.IAMProvider = iam.NewIAMProvider(ctx, 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) if err != nil { return nil, fmt.Errorf("failed to get AWS accounts: %w", err) } fm := preset.NewCisAwsOrganizationFetchers(ctx, log, ch, accounts, cache) m := make(registry.FetchersMap) for accountId, fetchersMap := range fm { for key, fetcher := range fetchersMap { m[fmt.Sprintf("%s-%s", accountId, key)] = fetcher } } return m, nil })) return reg, cloud.NewDataProvider(cloud.WithAccount(*awsIdentity)), nil, nil } func (a *AWSOrg) getAwsAccounts(ctx context.Context, log *clog.Logger, initialCfg awssdk.Config, rootIdentity *cloud.Identity) ([]preset.AwsAccount, error) { rootCfg := assumeRole( sts.NewFromConfig(initialCfg), initialCfg, fmtIAMRole(rootIdentity.Account, rootRole), ) stsClient := sts.NewFromConfig(rootCfg) // accountIdentities array contains all the Accounts and Organizational // Units, even if they are nested. accountIdentities, err := a.AccountProvider.ListAccounts(ctx, log, rootCfg) if err != nil { return nil, err } accounts := make([]preset.AwsAccount, 0, len(accountIdentities)) for _, identity := range accountIdentities { // Cloudbeat fetchers will try to assume memberRole // ("cloudbeat-securityaudit") for all Accounts and OUs except for the // Management Account. However, Cloud Formation only creates the // memberRole in the OUs chosen by the user. If Cloudbeat tries to // assume a member role that doesn't exist (because the user hasn't // selected an Account/OU), it will fail silently and will be unable to // retrieve any resources from the Account/OU afterward. var awsConfig awssdk.Config if identity.Account == rootIdentity.Account { cfg, err := a.pickManagementAccountRole(ctx, log, stsClient, rootCfg, identity) if err != nil { log.Errorf("error picking roles for account %s: %s", identity.Account, err) continue } awsConfig = cfg } else { // Try to assume "cloudbeat-security" and fail silently if it does // not exist. awsConfig = assumeRole( stsClient, rootCfg, fmtIAMRole(identity.Account, memberRole), ) } accounts = append(accounts, preset.AwsAccount{ Identity: identity, Config: awsConfig, }) } return accounts, nil } // pickManagementAccountRole selects role used to fetch resources from the // Management Account (and decides if they should be fetched at all). func (a *AWSOrg) pickManagementAccountRole(ctx context.Context, log *clog.Logger, stsClient stscreds.AssumeRoleAPIClient, rootCfg awssdk.Config, identity cloud.Identity) (awssdk.Config, error) { // We will check for a tag on 'cloudbeat-root' role. If it is missing, we // will try to be backward compatible and use the "cloudbeat-root" role to // scan the Management Account. In previous CF templates, "cloudbeat-root" // had the built-in SecurityAudit policy attached. var foundTagValue string { r, err := a.IAMProvider.GetRole(ctx, rootRole) if err != nil { return awssdk.Config{}, fmt.Errorf("error getting root role: %w", err) } for _, tag := range r.Tags { if pointers.Deref(tag.Key) == scanSettingTagKey { foundTagValue = pointers.Deref(tag.Value) break } } } if foundTagValue == "" { // Legacy. Use 'cloudbeat-root' role for compliance reasons. log.Infof("%q tag not found, using '%s' role for backward compatibility", scanSettingTagKey, rootRole) return rootCfg, nil } // Log an error if 'cloudbeat-securityaudit' does not exist in the // Management Account. This should not happen! We log and continue // without exiting function, since we want to scan other selected // accounts, but at least the error will be visible in the logs. if foundTagValue == scanSettingTagValue { _, err := a.IAMProvider.GetRole(ctx, memberRole) if err != nil { log.Errorf("Management Account should be scanned (%s: %s), but %q role is missing: %s", scanSettingTagKey, foundTagValue, memberRole, err) } } // If the "cloudbeat_scan_management_account" tag on the "cloudbeat-root" // role is set to "Yes", the user chose to scan it, and there should be a // "cloudbeat-securityaudit" role enabling this. If it is set to "No" we // will still try to use "cloudbeat-securityaudit", but it is non-existent, // so we will fail silently and not get any data from the Management // Account. log.Debugf("assuming '%s' role for Account %s", memberRole, identity.Account) config := assumeRole( stsClient, rootCfg, fmtIAMRole(identity.Account, memberRole), ) return config, nil } func (a *AWSOrg) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) { var awsConfig *awssdk.Config var err error if cfg.CloudConfig.Aws.CloudConnectors { awsConfig, err = awslib.InitializeAWSConfigCloudConnectors(ctx, cfg.CloudConfig.Aws) } else { awsConfig, err = awslib.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") } if a.IdentityProvider == nil { return errors.New("aws identity provider is uninitialized") } if a.AccountProvider == nil { return errors.New("aws account provider is uninitialized") } return nil } func assumeRole(client stscreds.AssumeRoleAPIClient, cfg awssdk.Config, arn string) awssdk.Config { cfg.Credentials = awssdk.NewCredentialsCache(stscreds.NewAssumeRoleProvider(client, arn)) return cfg } func fmtIAMRole(account string, role string) string { return fmt.Sprintf("arn:aws:iam::%s:role/%s", account, role) }