diff --git a/deploy/cloudformation/ec2-types.yml b/deploy/cloudformation/ec2-types.yml index 24684aeeff..3e94fc7126 100644 --- a/deploy/cloudformation/ec2-types.yml +++ b/deploy/cloudformation/ec2-types.yml @@ -27,7 +27,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId InstanceType: t4g.nano ImageId: ami-0a0ae3c8519bff7f0 BlockDeviceMappings: @@ -48,7 +48,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId InstanceType: t4g.small ImageId: ami-062e673cc4273dad8 BlockDeviceMappings: @@ -69,7 +69,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId InstanceType: t2.nano ImageId: ami-09ee771fad415a6d7 BlockDeviceMappings: @@ -90,7 +90,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId InstanceType: t2.nano ImageId: ami-00aa9d3df94c6c354 BlockDeviceMappings: @@ -111,7 +111,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId InstanceType: t2.nano ImageId: ami-089f338f3a2e69431 BlockDeviceMappings: @@ -132,7 +132,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId InstanceType: t2.nano ImageId: ami-04b1c88a6bbd48f8e BlockDeviceMappings: @@ -151,6 +151,6 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId GroupDescription: Block incoming traffic SecurityGroupIngress: [] diff --git a/deploy/cloudformation/elastic-agent-ec2-cnvm.yml b/deploy/cloudformation/elastic-agent-ec2-cnvm.yml index 671173a3f8..cbd821d071 100644 --- a/deploy/cloudformation/elastic-agent-ec2-cnvm.yml +++ b/deploy/cloudformation/elastic-agent-ec2-cnvm.yml @@ -46,7 +46,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId GroupDescription: Block incoming traffic SecurityGroupIngress: [] @@ -110,10 +110,10 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId Path: / Roles: - - !Ref "ElasticAgentRole" + - !Ref ElasticAgentRole # EC2 Instance to run elastic-agent ElasticAgentEc2Instance: @@ -131,7 +131,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId - Key: Task Value: Vulnerability Management Scanner ImageId: !Ref LatestAmiId diff --git a/deploy/cloudformation/elastic-agent-ec2-cspm-organization.yml b/deploy/cloudformation/elastic-agent-ec2-cspm-organization.yml index b3062b7e95..14fdb3adfa 100644 --- a/deploy/cloudformation/elastic-agent-ec2-cspm-organization.yml +++ b/deploy/cloudformation/elastic-agent-ec2-cspm-organization.yml @@ -11,6 +11,19 @@ Parameters: Type: CommaDelimitedList AllowedPattern: ^(ou-[0-9a-z]{4,32}-[a-z0-9]{8,32}|r-[0-9a-z]{4,32})$ + ScanManagementAccount: + Description: | + When set to "Yes", the Management Account resources will be scanned, + regardless of selected Organizational Unit IDs. Likewise, when set to + "No", the Management Account resources will not be scanned, even if + the Management Account belongs to a selected Organizational Unit. + Type: String + AllowedValues: + - "Yes" + - "No" + Default: "Yes" + ConstraintDescription: Must specify "Yes" or "No" + LatestAmiId: Type: AWS::SSM::Parameter::Value Default: /aws/service/ami-amazon-linux-latest/al2023-ami-minimal-kernel-default-arm64 @@ -41,6 +54,11 @@ Parameters: Description: The version of elastic-agent to install Type: String +Conditions: + ScanManagementAccountEnabled: !Equals + - !Ref ScanManagementAccount + - "Yes" + Resources: # Security Group for EC2 instance @@ -54,7 +72,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId GroupDescription: Block incoming traffic SecurityGroupIngress: [] @@ -64,6 +82,9 @@ Resources: Properties: RoleName: cloudbeat-root Description: Role that cloudbeat uses to assume roles in other accounts + Tags: + - Key: cloudbeat_scan_management_account + Value: !Ref ScanManagementAccount AssumeRolePolicyDocument: Version: "2012-10-17" Statement: @@ -84,6 +105,14 @@ Resources: PolicyDocument: Version: "2012-10-17" Statement: + - Effect: Allow + Action: + - iam:GetRole + - iam:ListAccountAliases + - iam:ListGroup + - iam:ListRoles + - iam:ListUsers + Resource: '*' - Effect: Allow Action: - organizations:List* @@ -93,8 +122,6 @@ Resources: Action: - sts:AssumeRole Resource: '*' - ManagedPolicyArns: - - arn:aws:iam::aws:policy/SecurityAudit # Instance profile to attach to EC2 instance ElasticAgentInstanceProfile: @@ -107,10 +134,10 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId Path: / Roles: - - !Ref "CloudbeatRootRole" + - !Ref CloudbeatRootRole # EC2 Instance to run elastic-agent ElasticAgentEc2Instance: @@ -128,7 +155,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId - Key: Task Value: Organization Cloud Security Posture Management Scanner ImageId: !Ref LatestAmiId @@ -208,6 +235,23 @@ Resources: ManagedPolicyArns: - arn:aws:iam::aws:policy/SecurityAudit + CloudbeatManagementAccountAuditRole: + Type: AWS::IAM::Role + Properties: + RoleName: cloudbeat-securityaudit + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + AWS: !GetAtt CloudbeatRootRole.Arn + Action: + - sts:AssumeRole + Path: / + ManagedPolicyArns: + - arn:aws:iam::aws:policy/SecurityAudit + Condition: ScanManagementAccountEnabled + Outputs: CloudbeatRootRoleArn: Description: The cloudbeat IAM role in the management account diff --git a/deploy/cloudformation/elastic-agent-ec2-cspm.yml b/deploy/cloudformation/elastic-agent-ec2-cspm.yml index 7dc7a0f060..f5efbcda9a 100644 --- a/deploy/cloudformation/elastic-agent-ec2-cspm.yml +++ b/deploy/cloudformation/elastic-agent-ec2-cspm.yml @@ -46,7 +46,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId GroupDescription: Block incoming traffic SecurityGroupIngress: [] @@ -67,6 +67,23 @@ Resources: ManagedPolicyArns: - arn:aws:iam::aws:policy/SecurityAudit + # IAM Role to assume for Management Account + CloudbeatRootRole: + Type: AWS::IAM::Role + Properties: + RoleName: cloudbeat-root + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + AWS: !GetAtt ElasticAgentRole.Arn + Action: + - sts:AssumeRole + Path: / + ManagedPolicyArns: + - arn:aws:iam::aws:policy/SecurityAudit + # Instance profile to attach to EC2 instance ElasticAgentInstanceProfile: Type: AWS::IAM::InstanceProfile @@ -78,10 +95,10 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId Path: / Roles: - - !Ref "ElasticAgentRole" + - !Ref ElasticAgentRole # EC2 Instance to run elastic-agent ElasticAgentEc2Instance: @@ -99,7 +116,7 @@ Resources: - 2 - !Split - / - - !Ref "AWS::StackId" + - !Ref AWS::StackId - Key: Task Value: Cloud Security Posture Management Scanner ImageId: !Ref LatestAmiId diff --git a/internal/flavors/benchmark/aws_org.go b/internal/flavors/benchmark/aws_org.go index deef65f9b7..51ce8b12a1 100644 --- a/internal/flavors/benchmark/aws_org.go +++ b/internal/flavors/benchmark/aws_org.go @@ -36,9 +36,19 @@ import ( "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 } @@ -67,6 +77,8 @@ func (a *AWSOrg) initialize(ctx context.Context, log *logp.Logger, cfg *config.C return nil, nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err) } + a.IAMProvider = iam.NewIAMProvider(log, awsConfig, nil) + awsIdentity, err := a.IdentityProvider.GetIdentity(ctx, awsConfig) if err != nil { return nil, nil, nil, fmt.Errorf("failed to get AWS identity: %w", err) @@ -95,11 +107,6 @@ func (a *AWSOrg) initialize(ctx context.Context, log *logp.Logger, cfg *config.C } func (a *AWSOrg) getAwsAccounts(ctx context.Context, log *logp.Logger, initialCfg awssdk.Config, rootIdentity *cloud.Identity) ([]preset.AwsAccount, error) { - const ( - rootRole = "cloudbeat-root" - memberRole = "cloudbeat-securityaudit" - ) - rootCfg := assumeRole( sts.NewFromConfig(initialCfg), initialCfg, @@ -107,6 +114,8 @@ func (a *AWSOrg) getAwsAccounts(ctx context.Context, log *logp.Logger, initialCf ) 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 @@ -114,11 +123,26 @@ func (a *AWSOrg) getAwsAccounts(ctx context.Context, log *logp.Logger, initialCf accounts := make([]preset.AwsAccount, 0, len(accountIdentities)) for _, identity := range accountIdentities { - var memberCfg awssdk.Config + // 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 { - memberCfg = rootCfg + 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 { - memberCfg = assumeRole( + // Try to assume "cloudbeat-security" and fail silently if it does + // not exist. + awsConfig = assumeRole( stsClient, rootCfg, fmtIAMRole(identity.Account, memberRole), @@ -127,13 +151,70 @@ func (a *AWSOrg) getAwsAccounts(ctx context.Context, log *logp.Logger, initialCf accounts = append(accounts, preset.AwsAccount{ Identity: identity, - Config: memberCfg, + 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 *logp.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) 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") } @@ -143,7 +224,7 @@ func (a *AWSOrg) checkDependencies() error { return nil } -func assumeRole(client *sts.Client, cfg awssdk.Config, arn string) awssdk.Config { +func assumeRole(client stscreds.AssumeRoleAPIClient, cfg awssdk.Config, arn string) awssdk.Config { cfg.Credentials = awssdk.NewCredentialsCache(stscreds.NewAssumeRoleProvider(client, arn)) return cfg } diff --git a/internal/flavors/benchmark/aws_org_test.go b/internal/flavors/benchmark/aws_org_test.go index 0f27118f70..24f1d7ea51 100644 --- a/internal/flavors/benchmark/aws_org_test.go +++ b/internal/flavors/benchmark/aws_org_test.go @@ -18,19 +18,27 @@ package benchmark import ( + "bytes" "context" "errors" + "fmt" "testing" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/elastic/elastic-agent-libs/logp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "github.com/elastic/cloudbeat/internal/config" "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/resources/fetching" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" "github.com/elastic/cloudbeat/internal/resources/utils/testhelper" ) @@ -39,6 +47,7 @@ func TestAWSOrg_Initialize(t *testing.T) { tests := []struct { name string + iamProvider iam.RoleGetter identityProvider awslib.IdentityProviderGetter accountProvider awslib.AccountProviderAPI cfg config.Config @@ -47,28 +56,32 @@ func TestAWSOrg_Initialize(t *testing.T) { }{ { name: "nothing initialized", - wantErr: "aws identity provider is uninitialized", + wantErr: "aws iam provider is uninitialized", }, { name: "account provider uninitialized", + iamProvider: getMockIAMRoleGetter(nil), identityProvider: mockAwsIdentityProvider(nil), accountProvider: nil, wantErr: "account provider is uninitialized", }, { name: "identity provider error", + iamProvider: getMockIAMRoleGetter(nil), identityProvider: mockAwsIdentityProvider(errors.New("some error")), accountProvider: mockAccountProvider(errors.New("not this error")), wantErr: "some error", }, { name: "account provider error", + iamProvider: getMockIAMRoleGetter(nil), identityProvider: mockAwsIdentityProvider(nil), accountProvider: mockAccountProvider(errors.New("some error")), want: []string{}, }, { name: "no error", + iamProvider: getMockIAMRoleGetter(nil), identityProvider: mockAwsIdentityProvider(nil), accountProvider: mockAccountProvider(nil), want: []string{ @@ -95,6 +108,7 @@ func TestAWSOrg_Initialize(t *testing.T) { t.Parallel() testInitialize(t, &AWSOrg{ + IAMProvider: tt.iamProvider, IdentityProvider: tt.identityProvider, AccountProvider: tt.accountProvider, }, &tt.cfg, tt.wantErr, tt.want) @@ -146,10 +160,12 @@ func Test_getAwsAccounts(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := AWSOrg{ + IAMProvider: getMockIAMRoleGetter([]iam.Role{*makeRole("cloudbeat-root")}), IdentityProvider: nil, AccountProvider: tt.accountProvider, } - got, err := a.getAwsAccounts(context.Background(), nil, aws.Config{}, &tt.rootIdentity) + log := logp.NewLogger("test") + got, err := a.getAwsAccounts(context.Background(), log, aws.Config{}, &tt.rootIdentity) if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return @@ -165,6 +181,100 @@ func Test_getAwsAccounts(t *testing.T) { } } +type mockStsClient struct{} + +func (c *mockStsClient) AssumeRole(_ context.Context, _ *sts.AssumeRoleInput, _ ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) { + return &sts.AssumeRoleOutput{}, nil +} + +func Test_pickManagementAccountRole(t *testing.T) { + tests := []struct { + name string + roles []iam.Role + expectedLog string + expectedErrorMessage string + }{ + { + name: "success: cloudbeat-root is not tagged (backward compatibility)", + roles: []iam.Role{*makeRole("cloudbeat-root")}, + expectedLog: "using 'cloudbeat-root' role for backward compatibility", + }, + { + name: "success: cloudbeat_scan_management_account: Yes", + roles: []iam.Role{ + *makeRole("cloudbeat-root", scanSettingTagKey, "Yes"), + *makeRole("cloudbeat-securityaudit"), + }, + expectedLog: "assuming 'cloudbeat-securityaudit' role", + }, + { + name: "success: cloudbeat_scan_management_account: No", + roles: []iam.Role{ + *makeRole("cloudbeat-root", scanSettingTagKey, "No"), + }, + expectedLog: "assuming 'cloudbeat-securityaudit' role", + }, + { + name: "fail: cloudbeat-root does not exist", + roles: []iam.Role{}, + expectedErrorMessage: "role \"cloudbeat-root\" does not exist", + }, + { + name: "warn: cloudbeat_scan_management_account: Yes, but cloudbeat-securityaudit does not exist", + roles: []iam.Role{ + *makeRole("cloudbeat-root", scanSettingTagKey, "Yes"), + }, + expectedLog: fmt.Sprintf( + "should be scanned (%s: %s), but %q role is missing", + scanSettingTagKey, scanSettingTagValue, memberRole, + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AWSOrg{ + IAMProvider: getMockIAMRoleGetter(tt.roles), + IdentityProvider: mockAwsIdentityProvider(nil), + AccountProvider: mockAccountProvider(nil), + } + + // set up log capture + var log *logp.Logger + logCaptureBuf := &bytes.Buffer{} + { + replacement := zap.WrapCore(func(zapcore.Core) zapcore.Core { + return zapcore.NewCore( + zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), + zapcore.AddSync(logCaptureBuf), + zapcore.DebugLevel, + ) + }) + log = logp.NewLogger("test").WithOptions(replacement) + } + + stsClient := &mockStsClient{} + rootCfg := assumeRole(stsClient, aws.Config{}, "cloudbeat-root") + identity := cloud.Identity{ + Account: "123", + AccountAlias: "some-name", + } + + _, err := a.pickManagementAccountRole(context.Background(), log, stsClient, rootCfg, identity) + if tt.expectedLog != "" { + require.NotEmpty(t, logCaptureBuf, "expected logs, but captured none") + require.Contains(t, logCaptureBuf.String(), tt.expectedLog, "expected message not found") + } + if tt.expectedErrorMessage != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.expectedErrorMessage) + } else { + require.NoError(t, err) + } + }) + } +} + func mockAccountProvider(err error) *awslib.MockAccountProviderAPI { provider := awslib.MockAccountProviderAPI{} on := provider.EXPECT().ListAccounts(mock.Anything, mock.Anything, mock.Anything) @@ -190,3 +300,46 @@ func mockAccountProviderWithIdentities(identities []cloud.Identity) *awslib.Mock provider.EXPECT().ListAccounts(mock.Anything, mock.Anything, mock.Anything).Return(identities, nil) return &provider } + +type mockIAMProvider struct { + iam.MockRoleGetter + roles []iam.Role +} + +func getMockIAMRoleGetter(roles []iam.Role) iam.RoleGetter { + result := &mockIAMProvider{ + MockRoleGetter: iam.MockRoleGetter{}, + roles: roles, + } + on := result.MockRoleGetter.EXPECT().GetRole(mock.Anything, mock.AnythingOfType("string")) + on.RunAndReturn( + func(_ context.Context, roleName string) (*iam.Role, error) { + for _, role := range result.roles { + if *role.RoleName == roleName { + return &role, nil + } + } + return nil, fmt.Errorf("role %q does not exist", roleName) + }, + ) + return result +} + +func makeRole(name string, tagKeyValues ...string) *iam.Role { + arn := "arn:aws:iam::123456789012" + name + tags := []types.Tag{} + for i := 0; i < len(tagKeyValues); i += 2 { + t := types.Tag{ + Key: &tagKeyValues[i], + Value: &tagKeyValues[i+1], + } + tags = append(tags, t) + } + return &iam.Role{ + Role: types.Role{ + Arn: &arn, + RoleName: &name, + Tags: tags, + }, + } +} diff --git a/internal/flavors/benchmark/strategy.go b/internal/flavors/benchmark/strategy.go index cc12ed346c..8f2e7e08e7 100644 --- a/internal/flavors/benchmark/strategy.go +++ b/internal/flavors/benchmark/strategy.go @@ -27,6 +27,7 @@ import ( "github.com/elastic/cloudbeat/internal/dataprovider/providers/k8s" "github.com/elastic/cloudbeat/internal/flavors/benchmark/builder" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" "github.com/elastic/cloudbeat/internal/resources/providers/azurelib" azure_auth "github.com/elastic/cloudbeat/internal/resources/providers/azurelib/auth" gcp_auth "github.com/elastic/cloudbeat/internal/resources/providers/gcplib/auth" @@ -43,6 +44,7 @@ func GetStrategy(cfg *config.Config) (Strategy, error) { case config.CIS_AWS: if cfg.CloudConfig.Aws.AccountType == config.OrganizationAccount { return &AWSOrg{ + IAMProvider: &iam.Provider{}, IdentityProvider: awslib.IdentityProvider{}, AccountProvider: awslib.AccountProvider{}, }, nil diff --git a/internal/resources/providers/awslib/iam/iam.go b/internal/resources/providers/awslib/iam/iam.go index e5dadd498f..5bbcb164f2 100644 --- a/internal/resources/providers/awslib/iam/iam.go +++ b/internal/resources/providers/awslib/iam/iam.go @@ -48,6 +48,7 @@ type Client interface { ListUserPolicies(ctx context.Context, params *iamsdk.ListUserPoliciesInput, optFns ...func(*iamsdk.Options)) (*iamsdk.ListUserPoliciesOutput, error) GetAccessKeyLastUsed(ctx context.Context, params *iamsdk.GetAccessKeyLastUsedInput, optFns ...func(*iamsdk.Options)) (*iamsdk.GetAccessKeyLastUsedOutput, error) GetAccountPasswordPolicy(ctx context.Context, params *iamsdk.GetAccountPasswordPolicyInput, optFns ...func(*iamsdk.Options)) (*iamsdk.GetAccountPasswordPolicyOutput, error) + GetRole(ctx context.Context, params *iamsdk.GetRoleInput, optFns ...func(*iamsdk.Options)) (*iamsdk.GetRoleOutput, error) GetRolePolicy(ctx context.Context, params *iamsdk.GetRolePolicyInput, optFns ...func(*iamsdk.Options)) (*iamsdk.GetRolePolicyOutput, error) GetCredentialReport(ctx context.Context, params *iamsdk.GetCredentialReportInput, optFns ...func(*iamsdk.Options)) (*iamsdk.GetCredentialReportOutput, error) GetUserPolicy(ctx context.Context, params *iamsdk.GetUserPolicyInput, optFns ...func(*iamsdk.Options)) (*iamsdk.GetUserPolicyOutput, error) @@ -137,6 +138,10 @@ type Policy struct { Roles []types.PolicyRole `json:"roles"` } +type Role struct { + types.Role +} + type ServerCertificatesInfo struct { Certificates []types.ServerCertificateMetadata `json:"certificates"` } diff --git a/internal/resources/providers/awslib/iam/mock_client.go b/internal/resources/providers/awslib/iam/mock_client.go index f2d33275fb..66f4d63f6d 100644 --- a/internal/resources/providers/awslib/iam/mock_client.go +++ b/internal/resources/providers/awslib/iam/mock_client.go @@ -459,6 +459,76 @@ func (_c *MockClient_GetPolicyVersion_Call) RunAndReturn(run func(context.Contex return _c } +// GetRole provides a mock function with given fields: ctx, params, optFns +func (_m *MockClient) GetRole(ctx context.Context, params *serviceiam.GetRoleInput, optFns ...func(*serviceiam.Options)) (*serviceiam.GetRoleOutput, error) { + _va := make([]interface{}, len(optFns)) + for _i := range optFns { + _va[_i] = optFns[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, params) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *serviceiam.GetRoleOutput + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *serviceiam.GetRoleInput, ...func(*serviceiam.Options)) (*serviceiam.GetRoleOutput, error)); ok { + return rf(ctx, params, optFns...) + } + if rf, ok := ret.Get(0).(func(context.Context, *serviceiam.GetRoleInput, ...func(*serviceiam.Options)) *serviceiam.GetRoleOutput); ok { + r0 = rf(ctx, params, optFns...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*serviceiam.GetRoleOutput) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *serviceiam.GetRoleInput, ...func(*serviceiam.Options)) error); ok { + r1 = rf(ctx, params, optFns...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_GetRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRole' +type MockClient_GetRole_Call struct { + *mock.Call +} + +// GetRole is a helper method to define mock.On call +// - ctx context.Context +// - params *serviceiam.GetRoleInput +// - optFns ...func(*serviceiam.Options) +func (_e *MockClient_Expecter) GetRole(ctx interface{}, params interface{}, optFns ...interface{}) *MockClient_GetRole_Call { + return &MockClient_GetRole_Call{Call: _e.mock.On("GetRole", + append([]interface{}{ctx, params}, optFns...)...)} +} + +func (_c *MockClient_GetRole_Call) Run(run func(ctx context.Context, params *serviceiam.GetRoleInput, optFns ...func(*serviceiam.Options))) *MockClient_GetRole_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]func(*serviceiam.Options), len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(func(*serviceiam.Options)) + } + } + run(args[0].(context.Context), args[1].(*serviceiam.GetRoleInput), variadicArgs...) + }) + return _c +} + +func (_c *MockClient_GetRole_Call) Return(_a0 *serviceiam.GetRoleOutput, _a1 error) *MockClient_GetRole_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_GetRole_Call) RunAndReturn(run func(context.Context, *serviceiam.GetRoleInput, ...func(*serviceiam.Options)) (*serviceiam.GetRoleOutput, error)) *MockClient_GetRole_Call { + _c.Call.Return(run) + return _c +} + // GetRolePolicy provides a mock function with given fields: ctx, params, optFns func (_m *MockClient) GetRolePolicy(ctx context.Context, params *serviceiam.GetRolePolicyInput, optFns ...func(*serviceiam.Options)) (*serviceiam.GetRolePolicyOutput, error) { _va := make([]interface{}, len(optFns)) diff --git a/internal/resources/providers/awslib/iam/mock_role_getter.go b/internal/resources/providers/awslib/iam/mock_role_getter.go new file mode 100644 index 0000000000..dfc02d60ca --- /dev/null +++ b/internal/resources/providers/awslib/iam/mock_role_getter.go @@ -0,0 +1,108 @@ +// 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. + +// Code generated by mockery v2.37.1. DO NOT EDIT. + +package iam + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockRoleGetter is an autogenerated mock type for the RoleGetter type +type MockRoleGetter struct { + mock.Mock +} + +type MockRoleGetter_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRoleGetter) EXPECT() *MockRoleGetter_Expecter { + return &MockRoleGetter_Expecter{mock: &_m.Mock} +} + +// GetRole provides a mock function with given fields: ctx, roleName +func (_m *MockRoleGetter) GetRole(ctx context.Context, roleName string) (*Role, error) { + ret := _m.Called(ctx, roleName) + + var r0 *Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*Role, error)); ok { + return rf(ctx, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *Role); ok { + r0 = rf(ctx, roleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRoleGetter_GetRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRole' +type MockRoleGetter_GetRole_Call struct { + *mock.Call +} + +// GetRole is a helper method to define mock.On call +// - ctx context.Context +// - roleName string +func (_e *MockRoleGetter_Expecter) GetRole(ctx interface{}, roleName interface{}) *MockRoleGetter_GetRole_Call { + return &MockRoleGetter_GetRole_Call{Call: _e.mock.On("GetRole", ctx, roleName)} +} + +func (_c *MockRoleGetter_GetRole_Call) Run(run func(ctx context.Context, roleName string)) *MockRoleGetter_GetRole_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockRoleGetter_GetRole_Call) Return(_a0 *Role, _a1 error) *MockRoleGetter_GetRole_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRoleGetter_GetRole_Call) RunAndReturn(run func(context.Context, string) (*Role, error)) *MockRoleGetter_GetRole_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRoleGetter creates a new instance of MockRoleGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRoleGetter(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRoleGetter { + mock := &MockRoleGetter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/resources/providers/awslib/iam/role.go b/internal/resources/providers/awslib/iam/role.go new file mode 100644 index 0000000000..ebd61d14e1 --- /dev/null +++ b/internal/resources/providers/awslib/iam/role.go @@ -0,0 +1,46 @@ +// 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 iam + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/iam" +) + +type RoleGetter interface { + GetRole(ctx context.Context, roleName string) (*Role, error) +} + +func (p Provider) GetRole(ctx context.Context, roleName string) (*Role, error) { + input := &iam.GetRoleInput{ + RoleName: &roleName, + } + + response, err := p.client.GetRole(ctx, input) + if err != nil { + return nil, fmt.Errorf("failed to get role %s - %w", roleName, err) + } + + r := &Role{ + Role: *response.Role, + } + + return r, nil +}