diff --git a/internal/flavors/asset_inventory.go b/internal/flavors/asset_inventory.go index b1281148d8..e9b5c0db23 100644 --- a/internal/flavors/asset_inventory.go +++ b/internal/flavors/asset_inventory.go @@ -29,7 +29,7 @@ import ( "github.com/elastic/cloudbeat/internal/config" "github.com/elastic/cloudbeat/internal/inventory" - awsinventory "github.com/elastic/cloudbeat/internal/inventory/aws" + "github.com/elastic/cloudbeat/internal/inventory/awsfetcher" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" ) @@ -92,7 +92,7 @@ func initAwsFetchers(ctx context.Context, cfg *config.Config, logger *logp.Logge return nil, err } - return awsinventory.Fetchers(logger, awsIdentity, awsConfig), nil + return awsfetcher.New(logger, awsIdentity, awsConfig), nil } func (bt *assetInventory) Run(*beat.Beat) error { diff --git a/internal/inventory/asset.go b/internal/inventory/asset.go index 8d322728c8..cb71a6fc7c 100644 --- a/internal/inventory/asset.go +++ b/internal/inventory/asset.go @@ -28,6 +28,7 @@ type assetCategory string const ( CategoryInfrastructure assetCategory = "infrastructure" + CategoryIdentity assetCategory = "identity" ) // assetSubCategory is used to build the document index. Use only numbers, letters and dashes (-) @@ -36,6 +37,8 @@ type assetSubCategory string const ( SubCategoryCompute assetSubCategory = "compute" SubCategoryStorage assetSubCategory = "storage" + + SubCategoryCloudProviderAccount assetSubCategory = "cloud-provider-account" ) // assetType is used to build the document index. Use only numbers, letters and dashes (-) @@ -44,6 +47,10 @@ type assetType string const ( TypeVirtualMachine assetType = "virtual-machine" TypeObjectStorage assetType = "object-storage" + + TypeUser assetType = "user" + TypeServiceAccount assetType = "service-account" + TypePermissions assetType = "permissions" ) // assetSubType is used to build the document index. Use only numbers, letters and dashes (-) @@ -52,6 +59,7 @@ type assetSubType string const ( SubTypeEC2 assetSubType = "ec2" SubTypeS3 assetSubType = "s3" + SubTypeIAM assetSubType = "iam" ) const ( @@ -187,6 +195,10 @@ func WithRawAsset(raw any) AssetEnricher { func WithTags(tags map[string]string) AssetEnricher { return func(a *AssetEvent) { + if len(tags) == 0 { + return + } + a.Asset.Tags = tags } } @@ -217,6 +229,10 @@ func WithIAM(iam AssetIAM) AssetEnricher { func WithResourcePolicies(policies ...AssetResourcePolicy) AssetEnricher { return func(a *AssetEvent) { + if len(policies) == 0 { + return + } + a.ResourcePolicies = policies } } diff --git a/internal/inventory/awsfetcher/awsfetchers.go b/internal/inventory/awsfetcher/awsfetchers.go new file mode 100644 index 0000000000..e4b184d026 --- /dev/null +++ b/internal/inventory/awsfetcher/awsfetchers.go @@ -0,0 +1,44 @@ +// 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 awsfetcher + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/elastic/elastic-agent-libs/logp" + + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" + "github.com/elastic/cloudbeat/internal/inventory" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/ec2" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/s3" +) + +func New(logger *logp.Logger, identity *cloud.Identity, cfg aws.Config) []inventory.AssetFetcher { + iamProvider := iam.NewIAMProvider(logger, cfg, &awslib.MultiRegionClientFactory[iam.AccessAnalyzerClient]{}) + ec2Provider := ec2.NewEC2Provider(logger, identity.Account, cfg, &awslib.MultiRegionClientFactory[ec2.Client]{}) + s3Provider := s3.NewProvider(logger, cfg, &awslib.MultiRegionClientFactory[s3.Client]{}, identity.Account) + + return []inventory.AssetFetcher{ + newEc2InstancesFetcher(logger, identity, ec2Provider), + newS3BucketFetcher(logger, identity, s3Provider), + newIamUserFetcher(logger, identity, iamProvider), + newIamRoleFetcher(logger, identity, iamProvider), + newIamPolicyFetcher(logger, identity, iamProvider), + } +} diff --git a/internal/inventory/aws/fetchers.go b/internal/inventory/awsfetcher/awsfetchers_test.go similarity index 55% rename from internal/inventory/aws/fetchers.go rename to internal/inventory/awsfetcher/awsfetchers_test.go index 2643296e71..024a5697f9 100644 --- a/internal/inventory/aws/fetchers.go +++ b/internal/inventory/awsfetcher/awsfetchers_test.go @@ -15,19 +15,38 @@ // specific language governing permissions and limitations // under the License. -package aws +package awsfetcher import ( - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/elastic/elastic-agent-libs/logp" + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" - "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/inventory" ) -func Fetchers(logger *logp.Logger, identity *cloud.Identity, cfg aws.Config) []inventory.AssetFetcher { - return []inventory.AssetFetcher{ - newEc2Fetcher(logger, identity, cfg), - NewS3BucketFetcher(logger, identity, cfg), +func collectResourcesAndMatch(t *testing.T, fetcher inventory.AssetFetcher, expected []inventory.AssetEvent) { + t.Helper() + + ch := make(chan inventory.AssetEvent) + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + go func() { + fetcher.Fetch(ctx, ch) + }() + + received := make([]inventory.AssetEvent, 0, len(expected)) + for len(expected) != len(received) { + select { + case <-ctx.Done(): + assert.ElementsMatch(t, expected, received) + return + case event := <-ch: + received = append(received, event) + } } + + assert.ElementsMatch(t, expected, received) } diff --git a/internal/inventory/aws/fetcher_ec2_instance.go b/internal/inventory/awsfetcher/fetcher_ec2_instance.go similarity index 87% rename from internal/inventory/aws/fetcher_ec2_instance.go rename to internal/inventory/awsfetcher/fetcher_ec2_instance.go index 983f2fa14a..6dffbe70cf 100644 --- a/internal/inventory/aws/fetcher_ec2_instance.go +++ b/internal/inventory/awsfetcher/fetcher_ec2_instance.go @@ -15,22 +15,20 @@ // specific language governing permissions and limitations // under the License. -package aws +package awsfetcher import ( "context" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/inventory" - "github.com/elastic/cloudbeat/internal/resources/providers/awslib" "github.com/elastic/cloudbeat/internal/resources/providers/awslib/ec2" "github.com/elastic/cloudbeat/internal/resources/utils/pointers" ) -type Ec2InstanceFetcher struct { +type ec2InstanceFetcher struct { logger *logp.Logger provider ec2InstancesProvider AccountId string @@ -48,9 +46,8 @@ var ec2InstanceClassification = inventory.AssetClassification{ SubType: inventory.SubTypeEC2, } -func newEc2Fetcher(logger *logp.Logger, identity *cloud.Identity, cfg aws.Config) inventory.AssetFetcher { - provider := ec2.NewEC2Provider(logger, identity.Account, cfg, &awslib.MultiRegionClientFactory[ec2.Client]{}) - return &Ec2InstanceFetcher{ +func newEc2InstancesFetcher(logger *logp.Logger, identity *cloud.Identity, provider ec2InstancesProvider) inventory.AssetFetcher { + return &ec2InstanceFetcher{ logger: logger, provider: provider, AccountId: identity.Account, @@ -58,7 +55,10 @@ func newEc2Fetcher(logger *logp.Logger, identity *cloud.Identity, cfg aws.Config } } -func (e *Ec2InstanceFetcher) Fetch(ctx context.Context, assetChannel chan<- inventory.AssetEvent) { +func (e *ec2InstanceFetcher) Fetch(ctx context.Context, assetChannel chan<- inventory.AssetEvent) { + e.logger.Info("Fetching EC2 Instances") + defer e.logger.Info("Fetching EC2 Instances - Finished") + instances, err := e.provider.DescribeInstances(ctx) if err != nil { e.logger.Errorf("Could not list ec2 instances: %v", err) @@ -125,7 +125,7 @@ func (e *Ec2InstanceFetcher) Fetch(ctx context.Context, assetChannel chan<- inve } } -func (e *Ec2InstanceFetcher) getTags(instance *ec2.Ec2Instance) map[string]string { +func (e *ec2InstanceFetcher) getTags(instance *ec2.Ec2Instance) map[string]string { tags := make(map[string]string, len(instance.Tags)) for _, t := range instance.Tags { if t.Key == nil { @@ -137,7 +137,7 @@ func (e *Ec2InstanceFetcher) getTags(instance *ec2.Ec2Instance) map[string]strin return tags } -func (e *Ec2InstanceFetcher) getAvailabilityZone(instance *ec2.Ec2Instance) *string { +func (e *ec2InstanceFetcher) getAvailabilityZone(instance *ec2.Ec2Instance) *string { if instance.Placement == nil { return nil } diff --git a/internal/inventory/aws/fetcher_ec2_instance_test.go b/internal/inventory/awsfetcher/fetcher_ec2_instance_test.go similarity index 87% rename from internal/inventory/aws/fetcher_ec2_instance_test.go rename to internal/inventory/awsfetcher/fetcher_ec2_instance_test.go index 4d5f0c3a63..cfe3f19daa 100644 --- a/internal/inventory/aws/fetcher_ec2_instance_test.go +++ b/internal/inventory/awsfetcher/fetcher_ec2_instance_test.go @@ -15,18 +15,16 @@ // specific language governing permissions and limitations // under the License. -package aws +package awsfetcher import ( - "context" "testing" - "time" "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/elastic/elastic-agent-libs/logp" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/inventory" ec2beat "github.com/elastic/cloudbeat/internal/resources/providers/awslib/ec2" "github.com/elastic/cloudbeat/internal/resources/utils/pointers" @@ -157,30 +155,8 @@ func TestEC2InstanceFetcher_Fetch(t *testing.T) { provider := newMockEc2InstancesProvider(t) provider.EXPECT().DescribeInstances(mock.Anything).Return(in, nil) - fetcher := Ec2InstanceFetcher{ - logger: logger, - provider: provider, - AccountId: "123", - AccountName: "alias", - } - - ch := make(chan inventory.AssetEvent) - ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) - defer cancel() - go func() { - fetcher.Fetch(ctx, ch) - }() - - received := make([]inventory.AssetEvent, 0, len(expected)) - for len(expected) != len(received) { - select { - case <-ctx.Done(): - assert.ElementsMatch(t, expected, received) - return - case event := <-ch: - received = append(received, event) - } - } + identity := &cloud.Identity{Account: "123", AccountAlias: "alias"} + fetcher := newEc2InstancesFetcher(logger, identity, provider) - assert.ElementsMatch(t, expected, received) + collectResourcesAndMatch(t, fetcher, expected) } diff --git a/internal/inventory/aws/fetcher_s3_bucket.go b/internal/inventory/awsfetcher/fetcher_iam_policy.go similarity index 66% rename from internal/inventory/aws/fetcher_s3_bucket.go rename to internal/inventory/awsfetcher/fetcher_iam_policy.go index 98c0b2196d..f863dbeb8f 100644 --- a/internal/inventory/aws/fetcher_s3_bucket.go +++ b/internal/inventory/awsfetcher/fetcher_iam_policy.go @@ -15,43 +15,40 @@ // specific language governing permissions and limitations // under the License. -package aws +package awsfetcher import ( "context" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/elastic/elastic-agent-libs/logp" - "github.com/samber/lo" "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/inventory" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" - "github.com/elastic/cloudbeat/internal/resources/providers/awslib/s3" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" "github.com/elastic/cloudbeat/internal/resources/utils/pointers" ) -type S3BucketFetcher struct { +type iamPolicyFetcher struct { logger *logp.Logger - provider s3BucketProvider + provider iamPolicyProvider AccountId string AccountName string } -var s3BucketClassification = inventory.AssetClassification{ - Category: inventory.CategoryInfrastructure, - SubCategory: inventory.SubCategoryStorage, - Type: inventory.TypeObjectStorage, - SubType: inventory.SubTypeS3, +type iamPolicyProvider interface { + GetPolicies(ctx context.Context) ([]awslib.AwsResource, error) } -type s3BucketProvider interface { - DescribeBuckets(ctx context.Context) ([]awslib.AwsResource, error) +var iamPolicyClassification = inventory.AssetClassification{ + Category: inventory.CategoryIdentity, + SubCategory: inventory.SubCategoryCloudProviderAccount, + Type: inventory.TypePermissions, + SubType: inventory.SubTypeIAM, } -func NewS3BucketFetcher(logger *logp.Logger, identity *cloud.Identity, cfg aws.Config) inventory.AssetFetcher { - provider := s3.NewProvider(logger, cfg, &awslib.MultiRegionClientFactory[s3.Client]{}, identity.Account) - return &S3BucketFetcher{ +func newIamPolicyFetcher(logger *logp.Logger, identity *cloud.Identity, provider iamPolicyProvider) inventory.AssetFetcher { + return &iamPolicyFetcher{ logger: logger, provider: provider, AccountId: identity.Account, @@ -59,53 +56,73 @@ func NewS3BucketFetcher(logger *logp.Logger, identity *cloud.Identity, cfg aws.C } } -func (s S3BucketFetcher) Fetch(ctx context.Context, assetChannel chan<- inventory.AssetEvent) { - awsBuckets, err := s.provider.DescribeBuckets(ctx) +func (i *iamPolicyFetcher) Fetch(ctx context.Context, assetChannel chan<- inventory.AssetEvent) { + i.logger.Info("Fetching IAM Policies") + defer i.logger.Info("Fetching IAM Policies - Finished") + + policies, err := i.provider.GetPolicies(ctx) if err != nil { - s.logger.Errorf("Could not list s3 buckets: %v", err) - if len(awsBuckets) == 0 { + i.logger.Errorf("Could not list policies: %v", err) + if len(policies) == 0 { return } } - buckets := lo.Map(awsBuckets, func(item awslib.AwsResource, _ int) s3.BucketDescription { - return item.(s3.BucketDescription) - }) + for _, resource := range policies { + if resource == nil { + continue + } + + policy, ok := resource.(iam.Policy) + if !ok { + i.logger.Errorf("Could not get info about policy: %s", resource.GetResourceArn()) + continue + } - for _, bucket := range buckets { assetChannel <- inventory.NewAssetEvent( - s3BucketClassification, - bucket.GetResourceArn(), - bucket.GetResourceName(), + iamPolicyClassification, + policy.GetResourceArn(), + resource.GetResourceName(), - inventory.WithRawAsset(bucket), + inventory.WithRawAsset(policy), + inventory.WithResourcePolicies(convertPolicy(policy.Document)...), + inventory.WithTags(i.getTags(policy)), inventory.WithCloud(inventory.AssetCloud{ Provider: inventory.AwsCloudProvider, - Region: bucket.Region, + Region: awslib.GlobalRegion, Account: inventory.AssetCloudAccount{ - Id: s.AccountId, - Name: s.AccountName, + Id: i.AccountId, + Name: i.AccountName, }, Service: &inventory.AssetCloudService{ - Name: "AWS S3", + Name: "AWS IAM", }, }), - inventory.WithResourcePolicies(getBucketPolicies(bucket)...), ) } } -func getBucketPolicies(bucket s3.BucketDescription) []inventory.AssetResourcePolicy { - if len(bucket.BucketPolicy) == 0 { +func (i *iamPolicyFetcher) getTags(policy iam.Policy) map[string]string { + tags := make(map[string]string, len(policy.Tags)) + + for _, tag := range policy.Tags { + tags[pointers.Deref(tag.Key)] = pointers.Deref(tag.Value) + } + + return tags +} + +func convertPolicy(policy map[string]any) []inventory.AssetResourcePolicy { + if len(policy) == 0 { return nil } - version, hasVersion := bucket.BucketPolicy["Version"].(string) + version, hasVersion := policy["Version"].(string) if !hasVersion { version = "" } - switch statements := bucket.BucketPolicy["Statement"].(type) { + switch statements := policy["Statement"].(type) { case []map[string]any: return convertStatements(statements, version) case []any: diff --git a/internal/inventory/awsfetcher/fetcher_iam_policy_test.go b/internal/inventory/awsfetcher/fetcher_iam_policy_test.go new file mode 100644 index 0000000000..63aec2b4bf --- /dev/null +++ b/internal/inventory/awsfetcher/fetcher_iam_policy_test.go @@ -0,0 +1,174 @@ +// 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 awsfetcher + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/stretchr/testify/mock" + + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" + "github.com/elastic/cloudbeat/internal/inventory" + "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" +) + +func TestIAMPolicyFetcher_Fetch(t *testing.T) { + now := time.Now() + + policy1 := iam.Policy{ + Policy: types.Policy{ + Arn: pointers.Ref("arn:aws:iam::0000:policy/policy-1"), + PolicyName: pointers.Ref("policy-1"), + PolicyId: pointers.Ref("178263"), + CreateDate: &now, + UpdateDate: &now, + Description: pointers.Ref("test"), + IsAttachable: true, + Path: pointers.Ref("/"), + Tags: []types.Tag{ + {Key: pointers.Ref("key-1"), Value: pointers.Ref("value-1")}, + {Key: pointers.Ref("key-2"), Value: pointers.Ref("value-2")}, + }, + }, + Document: map[string]any{ + "Version": "2012-10-17", + "Statement": []map[string]any{ + { + "Effect": "Allow", + "Action": []string{"read", "update", "delete"}, + "Resource": []string{"s3/bucket", "s3/bucket/*"}, + }, + { + "Effect": "Deny", + "Action": []string{"delete"}, + "Resource": []string{"s3/bucket"}, + }, + }, + }, + Roles: []types.PolicyRole{ + {RoleId: pointers.Ref("roleId-1"), RoleName: pointers.Ref("roleName-1")}, + {RoleId: pointers.Ref("roleId-2"), RoleName: pointers.Ref("roleName-2")}, + }, + } + + policy2 := iam.Policy{ + Policy: types.Policy{ + Arn: pointers.Ref("arn:aws:iam::0000:policy/policy-2"), + PolicyName: pointers.Ref("policy-2"), + Tags: []types.Tag{ + {Key: pointers.Ref("key-1"), Value: pointers.Ref("value-1")}, + }, + }, + Document: map[string]any{ + "Version": "2012-10-17", + "Statement": map[string]any{ + "Effect": "Allow", + "Action": "read", + "Resource": "*", + }, + }, + Roles: []types.PolicyRole{ + {RoleId: pointers.Ref("roleId-1"), RoleName: pointers.Ref("roleName-1")}, + }, + } + + policy3 := iam.Policy{ + Policy: types.Policy{ + Arn: pointers.Ref("arn:aws:iam::0000:policy/policy-3"), + PolicyName: pointers.Ref("policy-3"), + }, + } + + in := []awslib.AwsResource{policy1, nil, policy2, policy3} + + cloudField := inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: "global", + Account: inventory.AssetCloudAccount{ + Id: "123", + Name: "alias", + }, + Service: &inventory.AssetCloudService{ + Name: "AWS IAM", + }, + } + + expected := []inventory.AssetEvent{ + inventory.NewAssetEvent( + iamPolicyClassification, + "arn:aws:iam::0000:policy/policy-1", + "policy-1", + inventory.WithRawAsset(policy1), + inventory.WithCloud(cloudField), + inventory.WithTags(map[string]string{ + "key-1": "value-1", + "key-2": "value-2", + }), + inventory.WithResourcePolicies(inventory.AssetResourcePolicy{ + Version: pointers.Ref("2012-10-17"), + Effect: "Allow", + Action: []string{"read", "update", "delete"}, + Resource: []string{"s3/bucket", "s3/bucket/*"}, + }, inventory.AssetResourcePolicy{ + Version: pointers.Ref("2012-10-17"), + Effect: "Deny", + Action: []string{"delete"}, + Resource: []string{"s3/bucket"}, + }), + ), + + inventory.NewAssetEvent( + iamPolicyClassification, + "arn:aws:iam::0000:policy/policy-2", + "policy-2", + inventory.WithRawAsset(policy2), + inventory.WithCloud(cloudField), + inventory.WithTags(map[string]string{ + "key-1": "value-1", + }), + inventory.WithResourcePolicies(inventory.AssetResourcePolicy{ + Version: pointers.Ref("2012-10-17"), + Effect: "Allow", + Action: []string{"read"}, + Resource: []string{"*"}, + }), + ), + + inventory.NewAssetEvent( + iamPolicyClassification, + "arn:aws:iam::0000:policy/policy-3", + "policy-3", + inventory.WithRawAsset(policy3), + inventory.WithCloud(cloudField), + ), + } + + logger := logp.NewLogger("test_fetcher_iam_role") + provider := newMockIamPolicyProvider(t) + provider.EXPECT().GetPolicies(mock.Anything).Return(in, nil) + + identity := &cloud.Identity{Account: "123", AccountAlias: "alias"} + fetcher := newIamPolicyFetcher(logger, identity, provider) + + collectResourcesAndMatch(t, fetcher, expected) +} diff --git a/internal/inventory/awsfetcher/fetcher_iam_role.go b/internal/inventory/awsfetcher/fetcher_iam_role.go new file mode 100644 index 0000000000..d601af8f4b --- /dev/null +++ b/internal/inventory/awsfetcher/fetcher_iam_role.go @@ -0,0 +1,95 @@ +// 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 awsfetcher + +import ( + "context" + + "github.com/elastic/elastic-agent-libs/logp" + + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" + "github.com/elastic/cloudbeat/internal/inventory" + "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" +) + +type iamRoleFetcher struct { + logger *logp.Logger + provider iamRoleProvider + AccountId string + AccountName string +} + +type iamRoleProvider interface { + ListRoles(ctx context.Context) ([]*iam.Role, error) +} + +var iamRoleClassification = inventory.AssetClassification{ + Category: inventory.CategoryIdentity, + SubCategory: inventory.SubCategoryCloudProviderAccount, + Type: inventory.TypeServiceAccount, + SubType: inventory.SubTypeIAM, +} + +func newIamRoleFetcher(logger *logp.Logger, identity *cloud.Identity, provider iamRoleProvider) inventory.AssetFetcher { + return &iamRoleFetcher{ + logger: logger, + provider: provider, + AccountId: identity.Account, + AccountName: identity.AccountAlias, + } +} + +func (i *iamRoleFetcher) Fetch(ctx context.Context, assetChannel chan<- inventory.AssetEvent) { + i.logger.Info("Fetching IAM Roles") + defer i.logger.Info("Fetching IAM Roles - Finished") + + roles, err := i.provider.ListRoles(ctx) + if err != nil { + i.logger.Errorf("Could not list roles: %v", err) + if len(roles) == 0 { + return + } + } + + for _, role := range roles { + if role == nil { + continue + } + + assetChannel <- inventory.NewAssetEvent( + iamRoleClassification, + pointers.Deref(role.Arn), + pointers.Deref(role.RoleName), + + inventory.WithRawAsset(*role), + inventory.WithCloud(inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: awslib.GlobalRegion, + Account: inventory.AssetCloudAccount{ + Id: i.AccountId, + Name: i.AccountName, + }, + Service: &inventory.AssetCloudService{ + Name: "AWS IAM", + }, + }), + ) + } +} diff --git a/internal/inventory/awsfetcher/fetcher_iam_role_test.go b/internal/inventory/awsfetcher/fetcher_iam_role_test.go new file mode 100644 index 0000000000..a67350a979 --- /dev/null +++ b/internal/inventory/awsfetcher/fetcher_iam_role_test.go @@ -0,0 +1,117 @@ +// 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 awsfetcher + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/stretchr/testify/mock" + + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" + "github.com/elastic/cloudbeat/internal/inventory" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" + "github.com/elastic/cloudbeat/internal/resources/utils/pointers" +) + +func TestIAMRoleFetcher_Fetch(t *testing.T) { + now := time.Now() + + role1 := iam.Role{ + Role: types.Role{ + RoleName: pointers.Ref("role-name-1"), + Arn: pointers.Ref("arn:aws:iam::0000:role/role-name-1"), + RoleLastUsed: nil, + Tags: nil, + CreateDate: &now, + MaxSessionDuration: pointers.Ref(int32(3600)), + PermissionsBoundary: nil, + AssumeRolePolicyDocument: pointers.Ref("document"), + Description: pointers.Ref("EKS managed node group IAM role"), + Path: pointers.Ref("/"), + RoleId: pointers.Ref("17823618723"), + }, + } + + role2 := iam.Role{ + Role: types.Role{ + RoleName: pointers.Ref("role-name-2"), + Arn: pointers.Ref("arn:aws:iam::0000:role/role-name-2"), + RoleLastUsed: nil, + Tags: nil, + CreateDate: &now, + MaxSessionDuration: pointers.Ref(int32(3600)), + PermissionsBoundary: nil, + AssumeRolePolicyDocument: pointers.Ref("document"), + Description: pointers.Ref("EKS managed node group IAM role"), + Path: pointers.Ref("/"), + RoleId: pointers.Ref("17823618723"), + }, + } + + in := []*iam.Role{&role1, nil, &role2} + + expected := []inventory.AssetEvent{ + inventory.NewAssetEvent( + iamRoleClassification, + "arn:aws:iam::0000:role/role-name-1", + "role-name-1", + inventory.WithRawAsset(role1), + inventory.WithCloud(inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: "global", + Account: inventory.AssetCloudAccount{ + Id: "123", + Name: "alias", + }, + Service: &inventory.AssetCloudService{ + Name: "AWS IAM", + }, + }), + ), + + inventory.NewAssetEvent( + iamRoleClassification, + "arn:aws:iam::0000:role/role-name-2", + "role-name-2", + inventory.WithRawAsset(role2), + inventory.WithCloud(inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: "global", + Account: inventory.AssetCloudAccount{ + Id: "123", + Name: "alias", + }, + Service: &inventory.AssetCloudService{ + Name: "AWS IAM", + }, + }), + ), + } + + logger := logp.NewLogger("test_fetcher_iam_role") + provider := newMockIamRoleProvider(t) + provider.EXPECT().ListRoles(mock.Anything).Return(in, nil) + + identity := &cloud.Identity{Account: "123", AccountAlias: "alias"} + fetcher := newIamRoleFetcher(logger, identity, provider) + + collectResourcesAndMatch(t, fetcher, expected) +} diff --git a/internal/inventory/awsfetcher/fetcher_iam_user.go b/internal/inventory/awsfetcher/fetcher_iam_user.go new file mode 100644 index 0000000000..38d28731cd --- /dev/null +++ b/internal/inventory/awsfetcher/fetcher_iam_user.go @@ -0,0 +1,100 @@ +// 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 awsfetcher + +import ( + "context" + + "github.com/elastic/elastic-agent-libs/logp" + + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" + "github.com/elastic/cloudbeat/internal/inventory" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" +) + +type iamUserFetcher struct { + logger *logp.Logger + provider iamUserProvider + AccountId string + AccountName string +} + +type iamUserProvider interface { + GetUsers(ctx context.Context) ([]awslib.AwsResource, error) +} + +var iamUserClassification = inventory.AssetClassification{ + Category: inventory.CategoryIdentity, + SubCategory: inventory.SubCategoryCloudProviderAccount, + Type: inventory.TypeUser, + SubType: inventory.SubTypeIAM, +} + +func newIamUserFetcher(logger *logp.Logger, identity *cloud.Identity, provider iamUserProvider) inventory.AssetFetcher { + return &iamUserFetcher{ + logger: logger, + provider: provider, + AccountId: identity.Account, + AccountName: identity.AccountAlias, + } +} + +func (i *iamUserFetcher) Fetch(ctx context.Context, assetChannel chan<- inventory.AssetEvent) { + i.logger.Info("Fetching IAM Users") + defer i.logger.Info("Fetching IAM Users - Finished") + + users, err := i.provider.GetUsers(ctx) + if err != nil { + i.logger.Errorf("Could not list users: %v", err) + if len(users) == 0 { + return + } + } + + for _, resource := range users { + if resource == nil { + continue + } + + user, ok := resource.(iam.User) + if !ok { + i.logger.Errorf("Could not get info about user: %s", resource.GetResourceArn()) + continue + } + + assetChannel <- inventory.NewAssetEvent( + iamUserClassification, + user.GetResourceArn(), + user.GetResourceName(), + + inventory.WithRawAsset(user), + inventory.WithCloud(inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: user.GetRegion(), + Account: inventory.AssetCloudAccount{ + Id: i.AccountId, + Name: i.AccountName, + }, + Service: &inventory.AssetCloudService{ + Name: "AWS IAM", + }, + }), + ) + } +} diff --git a/internal/inventory/awsfetcher/fetcher_iam_user_test.go b/internal/inventory/awsfetcher/fetcher_iam_user_test.go new file mode 100644 index 0000000000..87b1ef2905 --- /dev/null +++ b/internal/inventory/awsfetcher/fetcher_iam_user_test.go @@ -0,0 +1,131 @@ +// 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 awsfetcher + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/stretchr/testify/mock" + + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" + "github.com/elastic/cloudbeat/internal/inventory" + "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" +) + +func TestIAMUserFetcher_Fetch(t *testing.T) { + now := time.Now() + + user1 := iam.User{ + Name: "user-1", + Arn: "arn:aws:iam::000:user/user-1", + LastAccess: "2023-03-28T12:27:26+00:00", + PasswordEnabled: true, + MfaActive: true, + PasswordLastChanged: "2023-03-28T12:27:26+00:00", + AccessKeys: []iam.AccessKey{ + { + Active: true, + HasUsed: true, + LastAccess: "2023-03-28T12:27:26+00:00", + RotationDate: "2023-03-28T12:27:26+00:00", + }, + }, + MFADevices: []iam.AuthDevice{ + { + IsVirtual: true, + MFADevice: types.MFADevice{ + EnableDate: &now, + SerialNumber: pointers.Ref("123"), + UserName: pointers.Ref("user-1"), + }, + }, + }, + InlinePolicies: []iam.PolicyDocument{ + { + PolicyName: "inline-policy", + Policy: "policy", + }, + }, + AttachedPolicies: []types.AttachedPolicy{ + { + PolicyArn: pointers.Ref("arn:aws:iam:1321312:policy/att-policy"), + PolicyName: pointers.Ref("att-policy"), + }, + }, + } + + user2 := iam.User{ + Name: "user-2", + Arn: "arn:aws:iam::000:user/user-2", + LastAccess: "2023-03-28T12:27:26+00:00", + } + + in := []awslib.AwsResource{user1, user2} + + expected := []inventory.AssetEvent{ + inventory.NewAssetEvent( + iamUserClassification, + "arn:aws:iam::000:user/user-1", + "user-1", + inventory.WithRawAsset(user1), + inventory.WithCloud(inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: "global", + Account: inventory.AssetCloudAccount{ + Id: "123", + Name: "alias", + }, + Service: &inventory.AssetCloudService{ + Name: "AWS IAM", + }, + }), + ), + + inventory.NewAssetEvent( + iamUserClassification, + "arn:aws:iam::000:user/user-2", + "user-2", + inventory.WithRawAsset(user2), + inventory.WithCloud(inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: "global", + Account: inventory.AssetCloudAccount{ + Id: "123", + Name: "alias", + }, + Service: &inventory.AssetCloudService{ + Name: "AWS IAM", + }, + }), + ), + } + + logger := logp.NewLogger("test_fetcher_iam_user") + provider := newMockIamUserProvider(t) + provider.EXPECT().GetUsers(mock.Anything).Return(in, nil) + + identity := &cloud.Identity{Account: "123", AccountAlias: "alias"} + fetcher := newIamUserFetcher(logger, identity, provider) + + collectResourcesAndMatch(t, fetcher, expected) +} diff --git a/internal/inventory/awsfetcher/fetcher_s3_bucket.go b/internal/inventory/awsfetcher/fetcher_s3_bucket.go new file mode 100644 index 0000000000..78fe69cefb --- /dev/null +++ b/internal/inventory/awsfetcher/fetcher_s3_bucket.go @@ -0,0 +1,96 @@ +// 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 awsfetcher + +import ( + "context" + + "github.com/elastic/elastic-agent-libs/logp" + "github.com/samber/lo" + + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" + "github.com/elastic/cloudbeat/internal/inventory" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib" + "github.com/elastic/cloudbeat/internal/resources/providers/awslib/s3" +) + +type s3BucketFetcher struct { + logger *logp.Logger + provider s3BucketProvider + AccountId string + AccountName string +} + +var s3BucketClassification = inventory.AssetClassification{ + Category: inventory.CategoryInfrastructure, + SubCategory: inventory.SubCategoryStorage, + Type: inventory.TypeObjectStorage, + SubType: inventory.SubTypeS3, +} + +type s3BucketProvider interface { + DescribeBuckets(ctx context.Context) ([]awslib.AwsResource, error) +} + +func newS3BucketFetcher(logger *logp.Logger, identity *cloud.Identity, provider s3BucketProvider) inventory.AssetFetcher { + return &s3BucketFetcher{ + logger: logger, + provider: provider, + AccountId: identity.Account, + AccountName: identity.AccountAlias, + } +} + +func (s *s3BucketFetcher) Fetch(ctx context.Context, assetChannel chan<- inventory.AssetEvent) { + s.logger.Info("Fetching S3 Bucket") + defer s.logger.Info("Fetching S3 Bucket - Finished") + + awsBuckets, err := s.provider.DescribeBuckets(ctx) + if err != nil { + s.logger.Errorf("Could not list s3 buckets: %v", err) + if len(awsBuckets) == 0 { + return + } + } + + buckets := lo.Map(awsBuckets, func(item awslib.AwsResource, _ int) s3.BucketDescription { + return item.(s3.BucketDescription) + }) + + for _, bucket := range buckets { + assetChannel <- inventory.NewAssetEvent( + s3BucketClassification, + bucket.GetResourceArn(), + bucket.GetResourceName(), + + inventory.WithRawAsset(bucket), + inventory.WithCloud(inventory.AssetCloud{ + Provider: inventory.AwsCloudProvider, + Region: bucket.Region, + Account: inventory.AssetCloudAccount{ + Id: s.AccountId, + Name: s.AccountName, + }, + Service: &inventory.AssetCloudService{ + Name: "AWS S3", + }, + }), + inventory.WithResourcePolicies(convertPolicy(bucket.BucketPolicy)...), + ) + } +} diff --git a/internal/inventory/aws/fetcher_s3_bucket_test.go b/internal/inventory/awsfetcher/fetcher_s3_bucket_test.go similarity index 88% rename from internal/inventory/aws/fetcher_s3_bucket_test.go rename to internal/inventory/awsfetcher/fetcher_s3_bucket_test.go index 5b22e57f94..6e6a48e593 100644 --- a/internal/inventory/aws/fetcher_s3_bucket_test.go +++ b/internal/inventory/awsfetcher/fetcher_s3_bucket_test.go @@ -15,19 +15,17 @@ // specific language governing permissions and limitations // under the License. -package aws +package awsfetcher import ( - "context" "testing" - "time" s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" s3ctrltypes "github.com/aws/aws-sdk-go-v2/service/s3control/types" "github.com/elastic/elastic-agent-libs/logp" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/inventory" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" "github.com/elastic/cloudbeat/internal/resources/providers/awslib/s3" @@ -169,30 +167,8 @@ func TestS3BucketFetcher_Fetch(t *testing.T) { provider := newMockS3BucketProvider(t) provider.EXPECT().DescribeBuckets(mock.Anything).Return(in, nil) - fetcher := S3BucketFetcher{ - logger: logger, - provider: provider, - AccountId: "123", - AccountName: "alias", - } - - ch := make(chan inventory.AssetEvent) - ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) - defer cancel() - go func() { - fetcher.Fetch(ctx, ch) - }() - - received := make([]inventory.AssetEvent, 0, len(expected)) - for len(expected) != len(received) { - select { - case <-ctx.Done(): - assert.ElementsMatch(t, expected, received) - return - case event := <-ch: - received = append(received, event) - } - } + identity := &cloud.Identity{Account: "123", AccountAlias: "alias"} + fetcher := newS3BucketFetcher(logger, identity, provider) - assert.ElementsMatch(t, expected, received) + collectResourcesAndMatch(t, fetcher, expected) } diff --git a/internal/inventory/aws/mock_ec2_instances_provider.go b/internal/inventory/awsfetcher/mock_ec2_instances_provider.go similarity index 99% rename from internal/inventory/aws/mock_ec2_instances_provider.go rename to internal/inventory/awsfetcher/mock_ec2_instances_provider.go index 1b6629da9c..ce01192c77 100644 --- a/internal/inventory/aws/mock_ec2_instances_provider.go +++ b/internal/inventory/awsfetcher/mock_ec2_instances_provider.go @@ -17,7 +17,7 @@ // Code generated by mockery v2.37.1. DO NOT EDIT. -package aws +package awsfetcher import ( context "context" diff --git a/internal/inventory/awsfetcher/mock_iam_policy_provider.go b/internal/inventory/awsfetcher/mock_iam_policy_provider.go new file mode 100644 index 0000000000..8dfa99a89d --- /dev/null +++ b/internal/inventory/awsfetcher/mock_iam_policy_provider.go @@ -0,0 +1,109 @@ +// 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 awsfetcher + +import ( + context "context" + + awslib "github.com/elastic/cloudbeat/internal/resources/providers/awslib" + + mock "github.com/stretchr/testify/mock" +) + +// mockIamPolicyProvider is an autogenerated mock type for the iamPolicyProvider type +type mockIamPolicyProvider struct { + mock.Mock +} + +type mockIamPolicyProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *mockIamPolicyProvider) EXPECT() *mockIamPolicyProvider_Expecter { + return &mockIamPolicyProvider_Expecter{mock: &_m.Mock} +} + +// GetPolicies provides a mock function with given fields: ctx +func (_m *mockIamPolicyProvider) GetPolicies(ctx context.Context) ([]awslib.AwsResource, error) { + ret := _m.Called(ctx) + + var r0 []awslib.AwsResource + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]awslib.AwsResource, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []awslib.AwsResource); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]awslib.AwsResource) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockIamPolicyProvider_GetPolicies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPolicies' +type mockIamPolicyProvider_GetPolicies_Call struct { + *mock.Call +} + +// GetPolicies is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockIamPolicyProvider_Expecter) GetPolicies(ctx interface{}) *mockIamPolicyProvider_GetPolicies_Call { + return &mockIamPolicyProvider_GetPolicies_Call{Call: _e.mock.On("GetPolicies", ctx)} +} + +func (_c *mockIamPolicyProvider_GetPolicies_Call) Run(run func(ctx context.Context)) *mockIamPolicyProvider_GetPolicies_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockIamPolicyProvider_GetPolicies_Call) Return(_a0 []awslib.AwsResource, _a1 error) *mockIamPolicyProvider_GetPolicies_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockIamPolicyProvider_GetPolicies_Call) RunAndReturn(run func(context.Context) ([]awslib.AwsResource, error)) *mockIamPolicyProvider_GetPolicies_Call { + _c.Call.Return(run) + return _c +} + +// newMockIamPolicyProvider creates a new instance of mockIamPolicyProvider. 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 newMockIamPolicyProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *mockIamPolicyProvider { + mock := &mockIamPolicyProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/inventory/awsfetcher/mock_iam_role_provider.go b/internal/inventory/awsfetcher/mock_iam_role_provider.go new file mode 100644 index 0000000000..b3e94c44f0 --- /dev/null +++ b/internal/inventory/awsfetcher/mock_iam_role_provider.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 awsfetcher + +import ( + context "context" + + iam "github.com/elastic/cloudbeat/internal/resources/providers/awslib/iam" + mock "github.com/stretchr/testify/mock" +) + +// mockIamRoleProvider is an autogenerated mock type for the iamRoleProvider type +type mockIamRoleProvider struct { + mock.Mock +} + +type mockIamRoleProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *mockIamRoleProvider) EXPECT() *mockIamRoleProvider_Expecter { + return &mockIamRoleProvider_Expecter{mock: &_m.Mock} +} + +// ListRoles provides a mock function with given fields: ctx +func (_m *mockIamRoleProvider) ListRoles(ctx context.Context) ([]*iam.Role, error) { + ret := _m.Called(ctx) + + var r0 []*iam.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*iam.Role, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*iam.Role); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*iam.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockIamRoleProvider_ListRoles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoles' +type mockIamRoleProvider_ListRoles_Call struct { + *mock.Call +} + +// ListRoles is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockIamRoleProvider_Expecter) ListRoles(ctx interface{}) *mockIamRoleProvider_ListRoles_Call { + return &mockIamRoleProvider_ListRoles_Call{Call: _e.mock.On("ListRoles", ctx)} +} + +func (_c *mockIamRoleProvider_ListRoles_Call) Run(run func(ctx context.Context)) *mockIamRoleProvider_ListRoles_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockIamRoleProvider_ListRoles_Call) Return(_a0 []*iam.Role, _a1 error) *mockIamRoleProvider_ListRoles_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockIamRoleProvider_ListRoles_Call) RunAndReturn(run func(context.Context) ([]*iam.Role, error)) *mockIamRoleProvider_ListRoles_Call { + _c.Call.Return(run) + return _c +} + +// newMockIamRoleProvider creates a new instance of mockIamRoleProvider. 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 newMockIamRoleProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *mockIamRoleProvider { + mock := &mockIamRoleProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/inventory/awsfetcher/mock_iam_user_provider.go b/internal/inventory/awsfetcher/mock_iam_user_provider.go new file mode 100644 index 0000000000..891099c164 --- /dev/null +++ b/internal/inventory/awsfetcher/mock_iam_user_provider.go @@ -0,0 +1,109 @@ +// 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 awsfetcher + +import ( + context "context" + + awslib "github.com/elastic/cloudbeat/internal/resources/providers/awslib" + + mock "github.com/stretchr/testify/mock" +) + +// mockIamUserProvider is an autogenerated mock type for the iamUserProvider type +type mockIamUserProvider struct { + mock.Mock +} + +type mockIamUserProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *mockIamUserProvider) EXPECT() *mockIamUserProvider_Expecter { + return &mockIamUserProvider_Expecter{mock: &_m.Mock} +} + +// GetUsers provides a mock function with given fields: ctx +func (_m *mockIamUserProvider) GetUsers(ctx context.Context) ([]awslib.AwsResource, error) { + ret := _m.Called(ctx) + + var r0 []awslib.AwsResource + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]awslib.AwsResource, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []awslib.AwsResource); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]awslib.AwsResource) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockIamUserProvider_GetUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUsers' +type mockIamUserProvider_GetUsers_Call struct { + *mock.Call +} + +// GetUsers is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockIamUserProvider_Expecter) GetUsers(ctx interface{}) *mockIamUserProvider_GetUsers_Call { + return &mockIamUserProvider_GetUsers_Call{Call: _e.mock.On("GetUsers", ctx)} +} + +func (_c *mockIamUserProvider_GetUsers_Call) Run(run func(ctx context.Context)) *mockIamUserProvider_GetUsers_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockIamUserProvider_GetUsers_Call) Return(_a0 []awslib.AwsResource, _a1 error) *mockIamUserProvider_GetUsers_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockIamUserProvider_GetUsers_Call) RunAndReturn(run func(context.Context) ([]awslib.AwsResource, error)) *mockIamUserProvider_GetUsers_Call { + _c.Call.Return(run) + return _c +} + +// newMockIamUserProvider creates a new instance of mockIamUserProvider. 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 newMockIamUserProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *mockIamUserProvider { + mock := &mockIamUserProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/inventory/aws/mock_s3_bucket_provider.go b/internal/inventory/awsfetcher/mock_s3_bucket_provider.go similarity index 99% rename from internal/inventory/aws/mock_s3_bucket_provider.go rename to internal/inventory/awsfetcher/mock_s3_bucket_provider.go index 1c9d19e3e0..d917494433 100644 --- a/internal/inventory/aws/mock_s3_bucket_provider.go +++ b/internal/inventory/awsfetcher/mock_s3_bucket_provider.go @@ -17,7 +17,7 @@ // Code generated by mockery v2.37.1. DO NOT EDIT. -package aws +package awsfetcher import ( context "context" diff --git a/internal/resources/providers/awslib/iam/iam.go b/internal/resources/providers/awslib/iam/iam.go index 5bbcb164f2..6beb7ff509 100644 --- a/internal/resources/providers/awslib/iam/iam.go +++ b/internal/resources/providers/awslib/iam/iam.go @@ -49,6 +49,7 @@ type Client interface { 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) + ListRoles(ctx context.Context, params *iamsdk.ListRolesInput, optFns ...func(*iamsdk.Options)) (*iamsdk.ListRolesOutput, 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) @@ -85,8 +86,8 @@ type User struct { LastAccess string `json:"last_access,omitempty"` Arn string `json:"arn,omitempty"` PasswordLastChanged string `json:"password_last_changed,omitempty"` - PasswordEnabled bool `json:"password_enabled"` MfaActive bool `json:"mfa_active"` + PasswordEnabled bool `json:"password_enabled"` } type AuthDevice struct { diff --git a/internal/resources/providers/awslib/iam/mock_client.go b/internal/resources/providers/awslib/iam/mock_client.go index 66f4d63f6d..538985a843 100644 --- a/internal/resources/providers/awslib/iam/mock_client.go +++ b/internal/resources/providers/awslib/iam/mock_client.go @@ -1089,6 +1089,76 @@ func (_c *MockClient_ListPolicies_Call) RunAndReturn(run func(context.Context, * return _c } +// ListRoles provides a mock function with given fields: ctx, params, optFns +func (_m *MockClient) ListRoles(ctx context.Context, params *serviceiam.ListRolesInput, optFns ...func(*serviceiam.Options)) (*serviceiam.ListRolesOutput, 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.ListRolesOutput + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *serviceiam.ListRolesInput, ...func(*serviceiam.Options)) (*serviceiam.ListRolesOutput, error)); ok { + return rf(ctx, params, optFns...) + } + if rf, ok := ret.Get(0).(func(context.Context, *serviceiam.ListRolesInput, ...func(*serviceiam.Options)) *serviceiam.ListRolesOutput); ok { + r0 = rf(ctx, params, optFns...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*serviceiam.ListRolesOutput) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *serviceiam.ListRolesInput, ...func(*serviceiam.Options)) error); ok { + r1 = rf(ctx, params, optFns...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_ListRoles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoles' +type MockClient_ListRoles_Call struct { + *mock.Call +} + +// ListRoles is a helper method to define mock.On call +// - ctx context.Context +// - params *serviceiam.ListRolesInput +// - optFns ...func(*serviceiam.Options) +func (_e *MockClient_Expecter) ListRoles(ctx interface{}, params interface{}, optFns ...interface{}) *MockClient_ListRoles_Call { + return &MockClient_ListRoles_Call{Call: _e.mock.On("ListRoles", + append([]interface{}{ctx, params}, optFns...)...)} +} + +func (_c *MockClient_ListRoles_Call) Run(run func(ctx context.Context, params *serviceiam.ListRolesInput, optFns ...func(*serviceiam.Options))) *MockClient_ListRoles_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.ListRolesInput), variadicArgs...) + }) + return _c +} + +func (_c *MockClient_ListRoles_Call) Return(_a0 *serviceiam.ListRolesOutput, _a1 error) *MockClient_ListRoles_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_ListRoles_Call) RunAndReturn(run func(context.Context, *serviceiam.ListRolesInput, ...func(*serviceiam.Options)) (*serviceiam.ListRolesOutput, error)) *MockClient_ListRoles_Call { + _c.Call.Return(run) + return _c +} + // ListServerCertificates provides a mock function with given fields: ctx, params, optFns func (_m *MockClient) ListServerCertificates(ctx context.Context, params *serviceiam.ListServerCertificatesInput, optFns ...func(*serviceiam.Options)) (*serviceiam.ListServerCertificatesOutput, error) { _va := make([]interface{}, len(optFns)) diff --git a/internal/resources/providers/awslib/iam/role.go b/internal/resources/providers/awslib/iam/role.go index ebd61d14e1..bc901b8e17 100644 --- a/internal/resources/providers/awslib/iam/role.go +++ b/internal/resources/providers/awslib/iam/role.go @@ -22,6 +22,8 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/samber/lo" ) type RoleGetter interface { @@ -44,3 +46,29 @@ func (p Provider) GetRole(ctx context.Context, roleName string) (*Role, error) { return r, nil } + +func (p Provider) ListRoles(ctx context.Context) ([]*Role, error) { + input := &iam.ListRolesInput{} + + roles := make([]types.Role, 0) + for { + nativeRoles, err := p.client.ListRoles(ctx, input) + if err != nil { + return nil, err + } + + roles = append(roles, nativeRoles.Roles...) + + if !nativeRoles.IsTruncated { + break + } + + input.Marker = nativeRoles.Marker + } + + return lo.Map(roles, func(role types.Role, _ int) *Role { + return &Role{ + Role: role, + } + }), nil +}