Skip to content

Commit fc57788

Browse files
moukoublenorestisfl
authored andcommitted
Status Handler; Expose aws permission issues to user using beats status. (#3515)
* Status Handler; Expose aws permission issues to user using beats status. * more tests * statushandler tests * fix test lint * fix test * linter fixes * fix docker file * cleanup * testcases * aws org flow: filter only the valid accounts * generate mocks * expand to asset inventory * change MultiRegionFetch error wrapping * fixes * nits
1 parent 4ffc36e commit fc57788

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1154
-263
lines changed

internal/flavors/asset_inventory.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/elastic/cloudbeat/internal/flavors/assetinventory"
2929
"github.com/elastic/cloudbeat/internal/infra/clog"
3030
"github.com/elastic/cloudbeat/internal/inventory"
31+
"github.com/elastic/cloudbeat/internal/statushandler"
3132
)
3233

3334
type assetInventory struct {
@@ -53,7 +54,9 @@ func newAssetInventoryFromCfg(b *beat.Beat, cfg *config.Config) (*assetInventory
5354
return nil, fmt.Errorf("failed to init client: %w", err)
5455
}
5556

56-
strategy := assetinventory.GetStrategy(logger, cfg)
57+
statusHandler := statushandler.NewStatusHandler(b.Manager)
58+
59+
strategy := assetinventory.GetStrategy(logger, cfg, statusHandler)
5760
newAssetInventory, err := strategy.NewAssetInventory(ctx, beatClient)
5861
if err != nil {
5962
cancel()

internal/flavors/assetinventory/strategy.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ import (
3636
gcp_auth "github.com/elastic/cloudbeat/internal/resources/providers/gcplib/auth"
3737
gcp_inventory "github.com/elastic/cloudbeat/internal/resources/providers/gcplib/inventory"
3838
"github.com/elastic/cloudbeat/internal/resources/providers/msgraph"
39+
"github.com/elastic/cloudbeat/internal/statushandler"
3940
)
4041

4142
type Strategy interface {
4243
NewAssetInventory(ctx context.Context, client beat.Client) (inventory.AssetInventory, error)
4344
}
4445

4546
type strategy struct {
46-
logger *clog.Logger
47-
cfg *config.Config
47+
logger *clog.Logger
48+
cfg *config.Config
49+
statusHandler statushandler.StatusHandlerAPI
4850
}
4951

5052
func (s *strategy) NewAssetInventory(ctx context.Context, client beat.Client) (inventory.AssetInventory, error) {
@@ -55,7 +57,7 @@ func (s *strategy) NewAssetInventory(ctx context.Context, client beat.Client) (i
5557
case config.ProviderAWS:
5658
switch s.cfg.CloudConfig.Aws.AccountType {
5759
case config.SingleAccount, config.OrganizationAccount:
58-
fetchers, err = s.initAwsFetchers(ctx)
60+
fetchers, err = s.initAwsFetchers(ctx, s.statusHandler)
5961
default:
6062
err = fmt.Errorf("unsupported account_type: %q", s.cfg.CloudConfig.Aws.AccountType)
6163
}
@@ -111,9 +113,10 @@ func (s *strategy) initGcpFetchers(ctx context.Context) ([]inventory.AssetFetche
111113
return gcpfetcher.New(s.logger, provider), nil
112114
}
113115

114-
func GetStrategy(logger *clog.Logger, cfg *config.Config) Strategy {
116+
func GetStrategy(logger *clog.Logger, cfg *config.Config, statusHandler statushandler.StatusHandlerAPI) Strategy {
115117
return &strategy{
116-
logger: logger,
117-
cfg: cfg,
118+
logger: logger,
119+
cfg: cfg,
120+
statusHandler: statusHandler,
118121
}
119122
}

internal/flavors/assetinventory/strategy_aws.go

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,16 @@ package assetinventory
2020
import (
2121
"context"
2222
"fmt"
23-
"strings"
2423

2524
awssdk "github.com/aws/aws-sdk-go-v2/aws"
2625
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
27-
"github.com/aws/aws-sdk-go-v2/service/s3"
2826
"github.com/aws/aws-sdk-go-v2/service/sts"
2927

3028
"github.com/elastic/cloudbeat/internal/config"
31-
"github.com/elastic/cloudbeat/internal/infra/clog"
3229
"github.com/elastic/cloudbeat/internal/inventory"
3330
"github.com/elastic/cloudbeat/internal/inventory/awsfetcher"
3431
"github.com/elastic/cloudbeat/internal/resources/providers/awslib"
35-
"github.com/elastic/cloudbeat/internal/resources/utils/pointers"
32+
"github.com/elastic/cloudbeat/internal/statushandler"
3633
)
3734

3835
const (
@@ -48,7 +45,7 @@ func (s *strategy) getInitialAWSConfig(ctx context.Context, cfg *config.Config)
4845
return awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred, s.logger)
4946
}
5047

51-
func (s *strategy) initAwsFetchers(ctx context.Context) ([]inventory.AssetFetcher, error) {
48+
func (s *strategy) initAwsFetchers(ctx context.Context, statusHandler statushandler.StatusHandlerAPI) ([]inventory.AssetFetcher, error) {
5249
awsConfig, err := s.getInitialAWSConfig(ctx, s.cfg)
5350
if err != nil {
5451
return nil, err
@@ -62,7 +59,7 @@ func (s *strategy) initAwsFetchers(ctx context.Context) ([]inventory.AssetFetche
6259

6360
// Early exit if we're scanning the entire account.
6461
if s.cfg.CloudConfig.Aws.AccountType == config.SingleAccount {
65-
return awsfetcher.New(ctx, s.logger, awsIdentity, *awsConfig), nil
62+
return awsfetcher.New(ctx, s.logger, awsIdentity, *awsConfig, statusHandler), nil
6663
}
6764

6865
// Assume audit roles per selected account and generate fetchers for them
@@ -84,30 +81,18 @@ func (s *strategy) initAwsFetchers(ctx context.Context) ([]inventory.AssetFetche
8481
rootRoleConfig,
8582
fmtIAMRole(identity.Account, memberRole),
8683
)
87-
if ok := tryListingBuckets(ctx, s.logger, assumedRoleConfig); !ok {
84+
if ok := awslib.CredentialsValid(ctx, assumedRoleConfig, s.logger); !ok {
8885
// role does not exist, skip identity/account
8986
s.logger.Infof("Skipping identity on purpose %+v", identity)
9087
continue
9188
}
92-
accountFetchers := awsfetcher.New(ctx, s.logger, &identity, assumedRoleConfig)
89+
accountFetchers := awsfetcher.New(ctx, s.logger, &identity, assumedRoleConfig, statusHandler)
9390
fetchers = append(fetchers, accountFetchers...)
9491
}
9592

9693
return fetchers, nil
9794
}
9895

99-
func tryListingBuckets(ctx context.Context, log *clog.Logger, roleConfig awssdk.Config) bool {
100-
s3Client := s3.NewFromConfig(roleConfig)
101-
_, err := s3Client.ListBuckets(ctx, &s3.ListBucketsInput{MaxBuckets: pointers.Ref(int32(1))})
102-
if err == nil {
103-
return true
104-
}
105-
if !strings.Contains(err.Error(), "not authorized to perform: sts:AssumeRole") {
106-
log.Errorf("Expected a 403 autorization error, but got: %v", err)
107-
}
108-
return false
109-
}
110-
11196
func assumeRole(client stscreds.AssumeRoleAPIClient, cfg awssdk.Config, arn string) awssdk.Config {
11297
cfg.Credentials = awssdk.NewCredentialsCache(stscreds.NewAssumeRoleProvider(client, arn))
11398
return cfg

internal/flavors/benchmark/aws.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import (
3333
"github.com/elastic/cloudbeat/internal/resources/fetching/preset"
3434
"github.com/elastic/cloudbeat/internal/resources/fetching/registry"
3535
"github.com/elastic/cloudbeat/internal/resources/providers/awslib"
36+
"github.com/elastic/cloudbeat/internal/statushandler"
3637
)
3738

3839
const resourceChBufferSize = 10000
3940

4041
type AWS struct {
4142
IdentityProvider awslib.IdentityProviderGetter
43+
StatusHandler statushandler.StatusHandlerAPI
4244
}
4345

4446
func (a *AWS) NewBenchmark(ctx context.Context, log *clog.Logger, cfg *config.Config) (builder.Benchmark, error) {
@@ -50,7 +52,7 @@ func (a *AWS) NewBenchmark(ctx context.Context, log *clog.Logger, cfg *config.Co
5052

5153
return builder.New(
5254
builder.WithBenchmarkDataProvider(bdp),
53-
).Build(ctx, log, cfg, resourceCh, reg)
55+
).Build(ctx, log, cfg, resourceCh, reg, a.StatusHandler)
5456
}
5557

5658
//revive:disable-next-line:function-result-limit
@@ -79,7 +81,7 @@ func (a *AWS) initialize(ctx context.Context, log *clog.Logger, cfg *config.Conf
7981

8082
return registry.NewRegistry(
8183
log,
82-
registry.WithFetchersMap(preset.NewCisAwsFetchers(ctx, log, *awsConfig, ch, awsIdentity)),
84+
registry.WithFetchersMap(preset.NewCisAwsFetchers(ctx, log, *awsConfig, ch, awsIdentity, a.StatusHandler)),
8385
), cloud.NewDataProvider(cloud.WithAccount(*awsIdentity)), nil, nil
8486
}
8587

internal/flavors/benchmark/aws_org.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
awssdk "github.com/aws/aws-sdk-go-v2/aws"
2727
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
2828
"github.com/aws/aws-sdk-go-v2/service/sts"
29+
"github.com/samber/lo"
2930
"go.opentelemetry.io/otel"
3031

3132
"github.com/elastic/cloudbeat/internal/config"
@@ -40,6 +41,7 @@ import (
4041
"github.com/elastic/cloudbeat/internal/resources/providers/awslib"
4142
"github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam"
4243
"github.com/elastic/cloudbeat/internal/resources/utils/pointers"
44+
"github.com/elastic/cloudbeat/internal/statushandler"
4345
)
4446

4547
const (
@@ -53,9 +55,11 @@ const (
5355
var tracer = otel.Tracer(scopeName)
5456

5557
type AWSOrg struct {
56-
IAMProvider iam.RoleGetter
57-
IdentityProvider awslib.IdentityProviderGetter
58-
AccountProvider awslib.AccountProviderAPI
58+
IAMProvider iam.RoleGetter
59+
IdentityProvider awslib.IdentityProviderGetter
60+
AccountProvider awslib.AccountProviderAPI
61+
StatusHandler statushandler.StatusHandlerAPI
62+
AWSCredsValidator awslib.CredentialsValidator
5963
}
6064

6165
func (a *AWSOrg) NewBenchmark(ctx context.Context, log *clog.Logger, cfg *config.Config) (builder.Benchmark, error) {
@@ -67,7 +71,7 @@ func (a *AWSOrg) NewBenchmark(ctx context.Context, log *clog.Logger, cfg *config
6771

6872
return builder.New(
6973
builder.WithBenchmarkDataProvider(bdp),
70-
).Build(ctx, log, cfg, resourceCh, reg)
74+
).Build(ctx, log, cfg, resourceCh, reg, a.StatusHandler)
7175
}
7276

7377
//revive:disable-next-line:function-result-limit
@@ -110,7 +114,13 @@ func (a *AWSOrg) initialize(ctx context.Context, log *clog.Logger, cfg *config.C
110114
return nil, observability.FailSpan(span, "failed to get AWS accounts", err)
111115
}
112116

113-
fm := preset.NewCisAwsOrganizationFetchers(ctx, spannedLog, ch, accounts, cache)
117+
// Filter the accounts to the ones having valid credentials on each aws account.
118+
// Meaning only the accounts that have the security audit role created and thus were selected by customer on cloud formation.
119+
filtered := lo.Filter(accounts, func(item preset.AwsAccount, _ int) bool {
120+
return a.AWSCredsValidator.Validate(ctx, item.Config, log)
121+
})
122+
123+
fm := preset.NewCisAwsOrganizationFetchers(ctx, spannedLog, ch, filtered, cache, a.StatusHandler)
114124
m := make(registry.FetchersMap)
115125
for accountId, fetchersMap := range fm {
116126
for key, fetcher := range fetchersMap {

internal/flavors/benchmark/aws_org_test.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam"
4141
"github.com/elastic/cloudbeat/internal/resources/utils/pointers"
4242
"github.com/elastic/cloudbeat/internal/resources/utils/testhelper"
43+
"github.com/elastic/cloudbeat/internal/statushandler"
4344
)
4445

4546
var expectedAWSSubtypes = []string{
@@ -116,9 +117,11 @@ func TestAWSOrg_Initialize(t *testing.T) {
116117
t.Parallel()
117118

118119
testInitialize(t, &AWSOrg{
119-
IAMProvider: tt.iamProvider,
120-
IdentityProvider: tt.identityProvider,
121-
AccountProvider: tt.accountProvider,
120+
IAMProvider: tt.iamProvider,
121+
IdentityProvider: tt.identityProvider,
122+
AccountProvider: tt.accountProvider,
123+
StatusHandler: statushandler.NewMockStatusHandlerAPI(t),
124+
AWSCredsValidator: awslib.CredentialsValidatorNOOP,
122125
}, &tt.cfg, tt.wantErr, tt.want)
123126
})
124127
}
@@ -168,9 +171,11 @@ func Test_getAwsAccounts(t *testing.T) {
168171
for _, tt := range tests {
169172
t.Run(tt.name, func(t *testing.T) {
170173
a := AWSOrg{
171-
IAMProvider: getMockIAMRoleGetter([]iam.Role{*makeRole("cloudbeat-root")}),
172-
IdentityProvider: nil,
173-
AccountProvider: tt.accountProvider,
174+
IAMProvider: getMockIAMRoleGetter([]iam.Role{*makeRole("cloudbeat-root")}),
175+
IdentityProvider: nil,
176+
AccountProvider: tt.accountProvider,
177+
StatusHandler: statushandler.NewMockStatusHandlerAPI(t),
178+
AWSCredsValidator: awslib.CredentialsValidatorNOOP,
174179
}
175180
log := testhelper.NewLogger(t)
176181
got, err := a.getAwsAccounts(t.Context(), log, aws.Config{}, &tt.rootIdentity)
@@ -245,9 +250,11 @@ func Test_pickManagementAccountRole(t *testing.T) {
245250
for _, tt := range tests {
246251
t.Run(tt.name, func(t *testing.T) {
247252
a := AWSOrg{
248-
IAMProvider: getMockIAMRoleGetter(tt.roles),
249-
IdentityProvider: mockAwsIdentityProvider(nil),
250-
AccountProvider: mockAccountProvider(nil),
253+
IAMProvider: getMockIAMRoleGetter(tt.roles),
254+
IdentityProvider: mockAwsIdentityProvider(nil),
255+
AccountProvider: mockAccountProvider(nil),
256+
StatusHandler: statushandler.NewMockStatusHandlerAPI(t),
257+
AWSCredsValidator: awslib.CredentialsValidatorNOOP,
251258
}
252259

253260
// set up log capture

internal/flavors/benchmark/aws_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/elastic/cloudbeat/internal/resources/fetching"
3333
"github.com/elastic/cloudbeat/internal/resources/providers/awslib"
3434
"github.com/elastic/cloudbeat/internal/resources/utils/testhelper"
35+
"github.com/elastic/cloudbeat/internal/statushandler"
3536
)
3637

3738
func TestAWS_Initialize(t *testing.T) {
@@ -159,6 +160,7 @@ func TestAWS_Initialize(t *testing.T) {
159160

160161
testInitialize(t, &AWS{
161162
IdentityProvider: tt.identityProvider,
163+
StatusHandler: statushandler.NewMockStatusHandlerAPI(t),
162164
}, &tt.cfg, tt.wantErr, tt.want)
163165
})
164166
}

internal/flavors/benchmark/azure.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/elastic/cloudbeat/internal/resources/fetching/registry"
3535
"github.com/elastic/cloudbeat/internal/resources/providers/azurelib"
3636
"github.com/elastic/cloudbeat/internal/resources/providers/azurelib/auth"
37+
"github.com/elastic/cloudbeat/internal/statushandler"
3738
)
3839

3940
type Azure struct {
@@ -51,7 +52,7 @@ func (a *Azure) NewBenchmark(ctx context.Context, log *clog.Logger, cfg *config.
5152
return builder.New(
5253
builder.WithBenchmarkDataProvider(bdp),
5354
builder.WithManagerTimeout(calculateFetcherTimeout(cfg.Period)),
54-
).Build(ctx, log, cfg, resourceCh, reg)
55+
).Build(ctx, log, cfg, resourceCh, reg, statushandler.NOOP{})
5556
}
5657

5758
//revive:disable-next-line:function-result-limit

internal/flavors/benchmark/builder/builder.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/elastic/cloudbeat/internal/resources/fetching"
3333
"github.com/elastic/cloudbeat/internal/resources/fetching/manager"
3434
"github.com/elastic/cloudbeat/internal/resources/fetching/registry"
35+
"github.com/elastic/cloudbeat/internal/statushandler"
3536
"github.com/elastic/cloudbeat/internal/transformer"
3637
"github.com/elastic/cloudbeat/version"
3738
)
@@ -63,12 +64,12 @@ func New(options ...Option) *Builder {
6364
return b
6465
}
6566

66-
func (b *Builder) Build(ctx context.Context, log *clog.Logger, cfg *config.Config, resourceCh chan fetching.ResourceInfo, reg registry.Registry) (Benchmark, error) {
67-
return b.buildBase(ctx, log, cfg, resourceCh, reg)
67+
func (b *Builder) Build(ctx context.Context, log *clog.Logger, cfg *config.Config, resourceCh chan fetching.ResourceInfo, reg registry.Registry, statusHandler statushandler.StatusHandlerAPI) (Benchmark, error) {
68+
return b.buildBase(ctx, log, cfg, resourceCh, reg, statusHandler)
6869
}
6970

70-
func (b *Builder) buildBase(ctx context.Context, log *clog.Logger, cfg *config.Config, resourceCh chan fetching.ResourceInfo, reg registry.Registry) (*basebenchmark, error) {
71-
manager, err := manager.NewManager(ctx, log, cfg.Period, b.managerTimeout, reg)
71+
func (b *Builder) buildBase(ctx context.Context, log *clog.Logger, cfg *config.Config, resourceCh chan fetching.ResourceInfo, reg registry.Registry, statusHandler statushandler.StatusHandlerAPI) (*basebenchmark, error) {
72+
manager, err := manager.NewManager(ctx, log, cfg.Period, b.managerTimeout, reg, statusHandler)
7273
if err != nil {
7374
return nil, err
7475
}

internal/flavors/benchmark/builder/builder_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/elastic/cloudbeat/internal/resources/fetching"
3232
"github.com/elastic/cloudbeat/internal/resources/fetching/registry"
3333
"github.com/elastic/cloudbeat/internal/resources/utils/testhelper"
34+
"github.com/elastic/cloudbeat/internal/statushandler"
3435
"github.com/elastic/cloudbeat/internal/uniqueness"
3536
)
3637

@@ -63,12 +64,15 @@ func TestBase_Build_Success(t *testing.T) {
6364
path, err := filepath.Abs("../../../../bundle.tar.gz")
6465
require.NoError(t, err)
6566

67+
sh := statushandler.NewMockStatusHandlerAPI(t)
68+
sh.EXPECT().Reset().Once()
69+
6670
resourceCh := make(chan fetching.ResourceInfo)
6771
reg := registry.NewMockRegistry(t)
6872
benchmark, err := New(tt.opts...).Build(t.Context(), log, &config.Config{
6973
BundlePath: path,
7074
Period: time.Minute,
71-
}, resourceCh, reg)
75+
}, resourceCh, reg, sh)
7276
require.NoError(t, err)
7377
assert.IsType(t, tt.benchType, benchmark)
7478

0 commit comments

Comments
 (0)