• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

elastic / cloudbeat / 15252389942

26 May 2025 11:06AM UTC coverage: 76.112% (-0.03%) from 76.137%
15252389942

Pull #3289

github

moukoublen
AWS CSPM ORG: initial operations fix. Use cloudbeat-root role
Pull Request #3289: AWS CSPM ORG: initial operations fix. Use cloudbeat-root role

41 of 51 new or added lines in 2 files covered. (80.39%)

1 existing line in 1 file now uncovered.

9186 of 12069 relevant lines covered (76.11%)

16.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.3
/internal/flavors/benchmark/aws_org.go
1
// Licensed to Elasticsearch B.V. under one or more contributor
2
// license agreements. See the NOTICE file distributed with
3
// this work for additional information regarding copyright
4
// ownership. Elasticsearch B.V. licenses this file to you under
5
// the Apache License, Version 2.0 (the "License"); you may
6
// not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
//     http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17

18
package benchmark
19

20
import (
21
        "context"
22
        "errors"
23
        "fmt"
24
        "strings"
25

26
        awssdk "github.com/aws/aws-sdk-go-v2/aws"
27
        "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
28
        "github.com/aws/aws-sdk-go-v2/service/sts"
29

30
        "github.com/elastic/cloudbeat/internal/config"
31
        "github.com/elastic/cloudbeat/internal/dataprovider"
32
        "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud"
33
        "github.com/elastic/cloudbeat/internal/flavors/benchmark/builder"
34
        "github.com/elastic/cloudbeat/internal/infra/clog"
35
        "github.com/elastic/cloudbeat/internal/resources/fetching"
36
        "github.com/elastic/cloudbeat/internal/resources/fetching/preset"
37
        "github.com/elastic/cloudbeat/internal/resources/fetching/registry"
38
        "github.com/elastic/cloudbeat/internal/resources/providers/awslib"
39
        "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam"
40
        "github.com/elastic/cloudbeat/internal/resources/utils/pointers"
41
)
42

43
const (
44
        rootRole            = "cloudbeat-root"
45
        memberRole          = "cloudbeat-securityaudit"
46
        scanSettingTagKey   = "cloudbeat_scan_management_account"
47
        scanSettingTagValue = "Yes"
48
)
49

50
type AWSOrg struct {
51
        IAMProvider      iam.RoleGetter
52
        IdentityProvider awslib.IdentityProviderGetter
53
        AccountProvider  awslib.AccountProviderAPI
54
}
55

56
func (a *AWSOrg) NewBenchmark(ctx context.Context, log *clog.Logger, cfg *config.Config) (builder.Benchmark, error) {
×
57
        resourceCh := make(chan fetching.ResourceInfo, resourceChBufferSize)
×
58
        reg, bdp, _, err := a.initialize(ctx, log, cfg, resourceCh)
×
59
        if err != nil {
×
60
                return nil, err
×
61
        }
×
62

63
        return builder.New(
×
64
                builder.WithBenchmarkDataProvider(bdp),
×
65
        ).Build(ctx, log, cfg, resourceCh, reg)
×
66
}
67

68
//revive:disable-next-line:function-result-limit
69
func (a *AWSOrg) initialize(ctx context.Context, log *clog.Logger, cfg *config.Config, ch chan fetching.ResourceInfo) (registry.Registry, dataprovider.CommonDataProvider, dataprovider.IdProvider, error) {
6✔
70
        if err := a.checkDependencies(); err != nil {
8✔
71
                return nil, nil, nil, err
2✔
72
        }
2✔
73

74
        var (
4✔
75
                awsConfigCloudbeatRoot *awssdk.Config
4✔
76
                awsIdentity            *cloud.Identity
4✔
77
                err                    error
4✔
78
        )
4✔
79

4✔
80
        // cloudbeat-root role credentials.
4✔
81
        awsConfigCloudbeatRoot, awsIdentity, err = a.getIdentity(ctx, cfg)
4✔
82
        if err != nil && cfg.CloudConfig.Aws.Cred.DefaultRegion == "" {
5✔
83
                log.Warn("failed to initialize identity; retrying to check AWS Gov Cloud regions")
1✔
84
                cfg.CloudConfig.Aws.Cred.DefaultRegion = awslib.DefaultGovRegion
1✔
85
                awsConfigCloudbeatRoot, awsIdentity, err = a.getIdentity(ctx, cfg)
1✔
86
        }
1✔
87

88
        if err != nil {
5✔
89
                return nil, nil, nil, fmt.Errorf("failed to get AWS Identity: %w", err)
1✔
90
        }
1✔
91
        log.Info("successfully retrieved AWS Identity")
3✔
92

3✔
93
        // IAMProvider which is iam.RoleGetter should be created using cloudbeat-root role credentials (requires iam:GetRole).
3✔
94
        a.IAMProvider = iam.NewIAMProvider(ctx, log, *awsConfigCloudbeatRoot, nil)
3✔
95

3✔
96
        cache := make(map[string]registry.FetchersMap)
3✔
97
        reg := registry.NewRegistry(log, registry.WithUpdater(
3✔
98
                func() (registry.FetchersMap, error) {
6✔
99
                        accounts, err := a.getAwsAccounts(ctx, log, *awsConfigCloudbeatRoot, awsIdentity)
3✔
100
                        if err != nil {
4✔
101
                                return nil, fmt.Errorf("failed to get AWS accounts: %w", err)
1✔
102
                        }
1✔
103

104
                        fm := preset.NewCisAwsOrganizationFetchers(ctx, log, ch, accounts, cache)
2✔
105
                        m := make(registry.FetchersMap)
2✔
106
                        for accountId, fetchersMap := range fm {
6✔
107
                                for key, fetcher := range fetchersMap {
32✔
108
                                        m[fmt.Sprintf("%s-%s", accountId, key)] = fetcher
28✔
109
                                }
28✔
110
                        }
111

112
                        return m, nil
2✔
113
                }))
114

115
        return reg, cloud.NewDataProvider(cloud.WithAccount(*awsIdentity)), nil, nil
3✔
116
}
117

118
// getAwsAccounts returns all the aws accounts of the org.
119
// For each account it bundles together the cloud.Identity and the credentials for the cloudbeat-securityaudit role of that account.
120
// It requires cloudbeat-root credentials (requires iam:ListAccountAliases and iam:GetRole).
121
func (a *AWSOrg) getAwsAccounts(ctx context.Context, log *clog.Logger, cfgCloudbeatRoot awssdk.Config, rootIdentity *cloud.Identity) ([]preset.AwsAccount, error) {
5✔
122
        stsClient := sts.NewFromConfig(cfgCloudbeatRoot)
5✔
123

5✔
124
        // accountIdentities array contains all the Accounts and Organizational
5✔
125
        // Units, even if they are nested. (requires iam:ListAccountAliases)
5✔
126
        accountIdentities, err := a.AccountProvider.ListAccounts(ctx, log, cfgCloudbeatRoot)
5✔
127
        if err != nil {
7✔
128
                return nil, err
2✔
129
        }
2✔
130

131
        accounts := make([]preset.AwsAccount, 0, len(accountIdentities))
3✔
132
        for _, identity := range accountIdentities {
9✔
133
                // Cloudbeat fetchers will try to assume memberRole
6✔
134
                // ("cloudbeat-securityaudit") for all Accounts and OUs except for the
6✔
135
                // Management Account. However, Cloud Formation only creates the
6✔
136
                // memberRole in the OUs chosen by the user. If Cloudbeat tries to
6✔
137
                // assume a member role that doesn't exist (because the user hasn't
6✔
138
                // selected an Account/OU), it will fail silently and will be unable to
6✔
139
                // retrieve any resources from the Account/OU afterward.
6✔
140
                var awsConfig awssdk.Config
6✔
141

6✔
142
                if identity.Account == rootIdentity.Account {
7✔
143
                        cfg, err := a.pickManagementAccountRole(ctx, log, stsClient, cfgCloudbeatRoot, identity)
1✔
144
                        if err != nil {
1✔
145
                                log.Errorf("error picking roles for account %s: %s", identity.Account, err)
×
146
                                continue
×
147
                        }
148
                        awsConfig = cfg
1✔
149
                } else {
5✔
150
                        // Try to assume "cloudbeat-security" and fail silently if it does
5✔
151
                        // not exist.
5✔
152
                        awsConfig = assumeRole(
5✔
153
                                stsClient,
5✔
154
                                cfgCloudbeatRoot,
5✔
155
                                fmtIAMRole(identity.Account, memberRole),
5✔
156
                        )
5✔
157
                }
5✔
158

159
                accounts = append(accounts, preset.AwsAccount{
6✔
160
                        Identity: identity,
6✔
161
                        Config:   awsConfig,
6✔
162
                })
6✔
163
        }
164
        return accounts, nil
3✔
165
}
166

167
// pickManagementAccountRole selects role used to fetch resources from the
168
// Management Account (and decides if they should be fetched at all).
169
func (a *AWSOrg) pickManagementAccountRole(ctx context.Context, log *clog.Logger, stsClient stscreds.AssumeRoleAPIClient, rootCfg awssdk.Config, identity cloud.Identity) (awssdk.Config, error) {
6✔
170
        // We will check for a tag on 'cloudbeat-root' role. If it is missing, we
6✔
171
        // will try to be backward compatible and use the "cloudbeat-root" role to
6✔
172
        // scan the Management Account. In previous CF templates, "cloudbeat-root"
6✔
173
        // had the built-in SecurityAudit policy attached.
6✔
174
        var foundTagValue string
6✔
175
        {
12✔
176
                r, err := a.IAMProvider.GetRole(ctx, rootRole)
6✔
177
                if err != nil {
7✔
178
                        return awssdk.Config{}, fmt.Errorf("error getting root role: %w", err)
1✔
179
                }
1✔
180

181
                for _, tag := range r.Tags {
8✔
182
                        if pointers.Deref(tag.Key) == scanSettingTagKey {
6✔
183
                                foundTagValue = pointers.Deref(tag.Value)
3✔
184
                                break
3✔
185
                        }
186
                }
187
        }
188

189
        if foundTagValue == "" {
7✔
190
                // Legacy. Use 'cloudbeat-root' role for compliance reasons.
2✔
191
                log.Infof("%q tag not found, using '%s' role for backward compatibility", scanSettingTagKey, rootRole)
2✔
192
                return rootCfg, nil
2✔
193
        }
2✔
194

195
        // Log an error if 'cloudbeat-securityaudit' does not exist in the
196
        // Management Account. This should not happen! We log and continue
197
        // without exiting function, since we want to scan other selected
198
        // accounts, but at least the error will be visible in the logs.
199
        if foundTagValue == scanSettingTagValue {
5✔
200
                _, err := a.IAMProvider.GetRole(ctx, memberRole)
2✔
201
                if err != nil {
3✔
202
                        log.Errorf("Management Account should be scanned (%s: %s), but %q role is missing: %s", scanSettingTagKey, foundTagValue, memberRole, err)
1✔
203
                }
1✔
204
        }
205

206
        // If the "cloudbeat_scan_management_account" tag on the "cloudbeat-root"
207
        // role is set to "Yes", the user chose to scan it, and there should be a
208
        // "cloudbeat-securityaudit" role enabling this. If it is set to "No" we
209
        // will still try to use "cloudbeat-securityaudit", but it is non-existent,
210
        // so we will fail silently and not get any data from the Management
211
        // Account.
212
        log.Debugf("assuming '%s' role for Account %s", memberRole, identity.Account)
3✔
213
        config := assumeRole(
3✔
214
                stsClient,
3✔
215
                rootCfg,
3✔
216
                fmtIAMRole(identity.Account, memberRole),
3✔
217
        )
3✔
218
        return config, nil
3✔
219
}
220

221
// getIdentity should assume the cloudbeat-root role and then perform the GetIdentity
222
// and return the aws config (having credentials from cloudbeat-root role) and cloud.Identity data.
223
func (a *AWSOrg) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) {
5✔
224
        awsConfig, err := a.getInitialAWSConfig(ctx, cfg)
5✔
225
        if err != nil {
5✔
NEW
226
                return nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err)
×
UNCOV
227
        }
×
228

229
        // Ensure cloudbeat-root role credentials.
230
        // Depending on case we might have or not have the GetRole policy, or we might already own the cloudbeat-root role,
231
        // case A [EC2 Instance]: in this case we have already assumed cloudbeat-root role automatically because of InstanceProfile.
232
        // case B [Direct Credentials]: in this case we have not assumed the cloudbeat-root role but we have the same policies with cloudbeat-root role, added to user.
233
        // case C [Cloud Connectors]: in this case we have not assumed the cloudbeat-root role nor have the same policies with cloudbeat-root.
234
        // So we will try to infer cloudbeat-root role ARN by using the same account id with our current identity.
235
        identity, err := a.IdentityProvider.GetCallerIdentity(ctx, *awsConfig)
5✔
236
        if err != nil {
5✔
NEW
237
                return nil, nil, fmt.Errorf("failed to initialize AWS credentials, failed to call GetCallerIdentity: %w", err)
×
NEW
238
        }
×
239

240
        var cfgCloudbeatRoot awssdk.Config
5✔
241

5✔
242
        if strings.Contains(pointers.Deref(identity.Arn), rootRole) {
6✔
243
                // case A [EC2 Instance] already cloudbeat-root, no need to re-assume.
1✔
244
                cfgCloudbeatRoot = *awsConfig
1✔
245
        } else {
5✔
246
                cfgCloudbeatRoot = assumeRole(
4✔
247
                        sts.NewFromConfig(*awsConfig),
4✔
248
                        *awsConfig,
4✔
249
                        fmtIAMRole(pointers.Deref(identity.Account), rootRole),
4✔
250
                )
4✔
251
        }
4✔
252

253
        // the next operation requires cloudbeat-root role (requires iam:ListAccountAliases policy).
254
        awsIdentity, err := a.IdentityProvider.GetIdentity(ctx, cfgCloudbeatRoot)
5✔
255
        if err != nil {
7✔
256
                return nil, nil, fmt.Errorf("failed to get AWS identity: %w", err)
2✔
257
        }
2✔
258

259
        return &cfgCloudbeatRoot, awsIdentity, nil
3✔
260
}
261

262
// getInitialAWSConfig return the initial aws.Config based on the received configuration.
263
func (a *AWSOrg) getInitialAWSConfig(ctx context.Context, cfg *config.Config) (*awssdk.Config, error) {
5✔
264
        if cfg.CloudConfig.Aws.CloudConnectors {
5✔
NEW
265
                // [Cloud Connectors] On cloud connectors this ends up assuming the customer remote role (using role chaining)
×
NEW
266
                return awslib.InitializeAWSConfigCloudConnectors(ctx, cfg.CloudConfig.Aws)
×
NEW
267
        }
×
268

269
        // [EC2 Instance] On EC2 created with our cloud formation, the identity is inferred by the EC2 instance InstanceProfile which has the cloudbeat-root role.
270
        // [Direct Credentials] On Direct credentials this identity is the user created by the cloud formation.
271
        // [Custom Setup] On custom setup like manual authentication for organization-level onboarding.
272
        return awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
5✔
273
}
274

275
func (a *AWSOrg) checkDependencies() error {
7✔
276
        if a.IAMProvider == nil {
8✔
277
                return errors.New("aws iam provider is uninitialized")
1✔
278
        }
1✔
279
        if a.IdentityProvider == nil {
6✔
280
                return errors.New("aws identity provider is uninitialized")
×
281
        }
×
282
        if a.AccountProvider == nil {
7✔
283
                return errors.New("aws account provider is uninitialized")
1✔
284
        }
1✔
285
        return nil
5✔
286
}
287

288
func assumeRole(client stscreds.AssumeRoleAPIClient, cfg awssdk.Config, arn string) awssdk.Config {
17✔
289
        cfg.Credentials = awssdk.NewCredentialsCache(stscreds.NewAssumeRoleProvider(client, arn))
17✔
290
        return cfg
17✔
291
}
17✔
292

293
func fmtIAMRole(account string, role string) string {
12✔
294
        return fmt.Sprintf("arn:aws:iam::%s:role/%s", account, role)
12✔
295
}
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc