Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 184 additions & 4 deletions collector/aws/collector/elasticache/elasticache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ package elasticache

import (
"context"
ec2Sdk "github.com/aws/aws-sdk-go-v2/service/ec2"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/elasticache"
"github.com/aws/aws-sdk-go-v2/service/elasticache/types"
"github.com/cloudrec/aws/collector"
"github.com/cloudrec/aws/collector/ec2"
"github.com/core-sdk/constant"
"github.com/core-sdk/log"
"github.com/core-sdk/schema"
"go.uber.org/zap"
"strings"
)

// GetElastiCacheClusterResource returns a ElastiCacheCluster Resource
Expand All @@ -44,12 +48,26 @@ func GetElastiCacheClusterResource() schema.Resource {

type CacheClusterDetail struct {
CacheCluster types.CacheCluster
// SecurityGroups includes detailed ingress/egress rules for linked VPC security groups.
SecurityGroups []ec2.SecurityGroupDetail
NetworkExposure ClusterNetworkExposure
}

type ClusterNetworkExposure struct {
CacheSubnetGroupName string
SubnetIDs []string
VpcID string
HasPublicSubnet bool
HasInternetGatewayRoute bool
HasEgressOnlyGatewayIPv6 bool
}

func GetCacheClusterDetail(ctx context.Context, service schema.ServiceInterface, res chan<- any) error {
client := service.(*collector.Services).ElastiCache
services := service.(*collector.Services)
elasticacheClient := services.ElastiCache
ec2Client := services.EC2

cacheClusterDetails, err := describeCacheClusterDetails(ctx, client)
cacheClusterDetails, err := describeCacheClusterDetails(ctx, elasticacheClient, ec2Client)
if err != nil {
log.CtxLogger(ctx).Warn("describeCacheClusterDetails error", zap.Error(err))
return err
Expand All @@ -61,21 +79,183 @@ func GetCacheClusterDetail(ctx context.Context, service schema.ServiceInterface,
return nil
}

func describeCacheClusterDetails(ctx context.Context, c *elasticache.Client) (cacheClusterDetails []CacheClusterDetail, err error) {
func describeCacheClusterDetails(ctx context.Context, c *elasticache.Client, ec2Client *ec2Sdk.Client) (cacheClusterDetails []CacheClusterDetail, err error) {
cacheClusters, err := describeCacheClusters(ctx, c)
if err != nil {
log.CtxLogger(ctx).Warn("describeCacheClusters error", zap.Error(err))
return nil, err
}
for _, cacheCluster := range cacheClusters {
securityGroups := describeClusterSecurityGroups(ctx, ec2Client, cacheCluster.SecurityGroups)
networkExposure := describeClusterNetworkExposure(ctx, c, ec2Client, cacheCluster.CacheSubnetGroupName)
cacheClusterDetails = append(cacheClusterDetails, CacheClusterDetail{
CacheCluster: cacheCluster,
CacheCluster: cacheCluster,
SecurityGroups: securityGroups,
NetworkExposure: networkExposure,
})
}

return cacheClusterDetails, nil
}

func describeClusterSecurityGroups(ctx context.Context, ec2Client *ec2Sdk.Client, groups []types.SecurityGroupMembership) []ec2.SecurityGroupDetail {
if ec2Client == nil || len(groups) == 0 {
return nil
}

groupIDs := make([]string, 0, len(groups))
for _, group := range groups {
if group.SecurityGroupId != nil && *group.SecurityGroupId != "" {
groupIDs = append(groupIDs, *group.SecurityGroupId)
}
}
if len(groupIDs) == 0 {
return nil
}

return ec2.DescribeSecurityGroupDetailsByFilters(ctx, ec2Client, []ec2Types.Filter{
{
Name: stringPtr("group-id"),
Values: groupIDs,
},
})
}

func describeClusterNetworkExposure(ctx context.Context, cacheClient *elasticache.Client, ec2Client *ec2Sdk.Client, cacheSubnetGroupName *string) ClusterNetworkExposure {
exposure := ClusterNetworkExposure{}
if cacheSubnetGroupName != nil {
exposure.CacheSubnetGroupName = *cacheSubnetGroupName
}
if ec2Client == nil || cacheClient == nil || cacheSubnetGroupName == nil || *cacheSubnetGroupName == "" {
return exposure
}

output, err := cacheClient.DescribeCacheSubnetGroups(ctx, &elasticache.DescribeCacheSubnetGroupsInput{
CacheSubnetGroupName: cacheSubnetGroupName,
})
if err != nil || output == nil || len(output.CacheSubnetGroups) == 0 {
if err != nil {
log.CtxLogger(ctx).Warn("describe cache subnet groups failed", zap.String("cacheSubnetGroup", *cacheSubnetGroupName), zap.Error(err))
}
return exposure
}

subnetGroup := output.CacheSubnetGroups[0]
if subnetGroup.VpcId != nil {
exposure.VpcID = *subnetGroup.VpcId
}

subnetIDs := make([]string, 0, len(subnetGroup.Subnets))
for _, s := range subnetGroup.Subnets {
if s.SubnetIdentifier != nil && *s.SubnetIdentifier != "" {
subnetIDs = append(subnetIDs, *s.SubnetIdentifier)
}
}
if len(subnetIDs) == 0 {
return exposure
}
exposure.SubnetIDs = subnetIDs

subnetOutput, err := ec2Client.DescribeSubnets(ctx, &ec2Sdk.DescribeSubnetsInput{SubnetIds: subnetIDs})
if err != nil {
log.CtxLogger(ctx).Warn("describe subnets failed", zap.String("cacheSubnetGroup", *cacheSubnetGroupName), zap.Error(err))
return exposure
}

subnetVpcMap := map[string]string{}
for _, subnet := range subnetOutput.Subnets {
if subnet.SubnetId == nil || *subnet.SubnetId == "" {
continue
}
subnetID := *subnet.SubnetId
if subnet.VpcId != nil {
subnetVpcMap[subnetID] = *subnet.VpcId
if exposure.VpcID == "" {
exposure.VpcID = *subnet.VpcId
}
}
if subnet.MapPublicIpOnLaunch != nil && *subnet.MapPublicIpOnLaunch {
exposure.HasPublicSubnet = true
}
}

for _, subnetID := range subnetIDs {
vpcID := subnetVpcMap[subnetID]
igwRoute, eigwRoute := subnetHasInternetRoute(ctx, ec2Client, subnetID, vpcID)
if igwRoute {
exposure.HasInternetGatewayRoute = true
}
if eigwRoute {
exposure.HasEgressOnlyGatewayIPv6 = true
}
if exposure.HasInternetGatewayRoute && exposure.HasEgressOnlyGatewayIPv6 {
break
}
}

return exposure
}

func subnetHasInternetRoute(ctx context.Context, ec2Client *ec2Sdk.Client, subnetID, vpcID string) (bool, bool) {
routeTables := describeRouteTablesByFilters(ctx, ec2Client, []ec2Types.Filter{{
Name: stringPtr("association.subnet-id"),
Values: []string{subnetID},
}})
if len(routeTables) == 0 && vpcID != "" {
routeTables = describeRouteTablesByFilters(ctx, ec2Client, []ec2Types.Filter{
{
Name: stringPtr("vpc-id"),
Values: []string{vpcID},
},
{
Name: stringPtr("association.main"),
Values: []string{"true"},
},
})
}

var hasIGW bool
var hasEIGW bool
for _, rt := range routeTables {
for _, r := range rt.Routes {
destinationIPv4 := r.DestinationCidrBlock != nil && *r.DestinationCidrBlock == "0.0.0.0/0"
destinationIPv6 := r.DestinationIpv6CidrBlock != nil && *r.DestinationIpv6CidrBlock == "::/0"

if destinationIPv4 && r.GatewayId != nil && strings.HasPrefix(*r.GatewayId, "igw-") {
hasIGW = true
}
if destinationIPv6 && r.EgressOnlyInternetGatewayId != nil && strings.HasPrefix(*r.EgressOnlyInternetGatewayId, "eigw-") {
hasEIGW = true
}
}
}
return hasIGW, hasEIGW
}

func describeRouteTablesByFilters(ctx context.Context, ec2Client *ec2Sdk.Client, filters []ec2Types.Filter) []ec2Types.RouteTable {
input := &ec2Sdk.DescribeRouteTablesInput{Filters: filters}
output, err := ec2Client.DescribeRouteTables(ctx, input)
if err != nil {
log.CtxLogger(ctx).Warn("describe route tables failed", zap.Error(err))
return nil
}
routeTables := append([]ec2Types.RouteTable{}, output.RouteTables...)
for output.NextToken != nil {
input.NextToken = output.NextToken
output, err = ec2Client.DescribeRouteTables(ctx, input)
if err != nil {
log.CtxLogger(ctx).Warn("describe route tables failed", zap.Error(err))
return routeTables
}
routeTables = append(routeTables, output.RouteTables...)
}
return routeTables
}

func stringPtr(v string) *string {
return &v
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This helper function stringPtr duplicates functionality from the AWS SDK. It's more idiomatic and consistent with other parts of the codebase to use aws.String() from the github.com/aws/aws-sdk-go-v2/aws package. Please remove this local helper and replace its usages with aws.String(). This will require adding an import for github.com/aws/aws-sdk-go-v2/aws.


func describeCacheClusters(ctx context.Context, c *elasticache.Client) (cacheClusters []types.CacheCluster, err error) {
input := &elasticache.DescribeCacheClustersInput{}
output, err := c.DescribeCacheClusters(ctx, input)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ func GetELBResource() schema.Resource {
type ELBDetail struct {
ELB types.LoadBalancer

// Listeners information of the LoadBalancer
Listeners []types.Listener

// SecurityGroups information of the LoadBalancer
SecurityGroups []ec2.SecurityGroupDetail

Expand Down Expand Up @@ -82,8 +85,10 @@ func describeELBDetails(ctx context.Context, elbClient *elasticloadbalancingv2.C
}

for _, elb := range elbs {
listeners := describeListenersByLoadBalancerArn(ctx, elbClient, elb.LoadBalancerArn)
ELBDetails = append(ELBDetails, ELBDetail{
ELB: elb,
ELB: elb,
Listeners: listeners,
VPC: ec2.DescribeVPCDetailsByFilters(ctx, ec2Client, []types2.Filter{
{
Name: aws.String("vpc-id"),
Expand All @@ -101,6 +106,36 @@ func describeELBDetails(ctx context.Context, elbClient *elasticloadbalancingv2.C
return ELBDetails, nil
}

func describeListenersByLoadBalancerArn(ctx context.Context, c *elasticloadbalancingv2.Client, loadBalancerArn *string) []types.Listener {
if loadBalancerArn == nil || *loadBalancerArn == "" {
return nil
}

var listeners []types.Listener
input := &elasticloadbalancingv2.DescribeListenersInput{
LoadBalancerArn: loadBalancerArn,
}

output, err := c.DescribeListeners(ctx, input)
if err != nil {
log.CtxLogger(ctx).Warn("DescribeListeners error", zap.Error(err))
return nil
}

listeners = append(listeners, output.Listeners...)
for output.NextMarker != nil {
input.Marker = output.NextMarker
output, err = c.DescribeListeners(ctx, input)
if err != nil {
log.CtxLogger(ctx).Warn("DescribeListeners error", zap.Error(err))
break
}
listeners = append(listeners, output.Listeners...)
}

return listeners
}

func describeELBs(ctx context.Context, c *elasticloadbalancingv2.Client) (elbs []types.LoadBalancer, err error) {
input := &elasticloadbalancingv2.DescribeLoadBalancersInput{
PageSize: aws.Int32(400),
Expand Down
35 changes: 29 additions & 6 deletions collector/aws/collector/iam/account_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package iam

import (
"context"
accessanalyzerSvc "github.com/aws/aws-sdk-go-v2/service/accessanalyzer"
accessanalyzerTypes "github.com/aws/aws-sdk-go-v2/service/accessanalyzer/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/cloudrec/aws/collector"
Expand Down Expand Up @@ -50,14 +52,17 @@ type AccountSettingsDetail struct {

AccountId string

AccessAnalyzers []accessanalyzerTypes.AnalyzerSummary

// todo
//EnabledFeatures []types.FeatureType
}

func GetAccountSettingsDetail(ctx context.Context, service schema.ServiceInterface, res chan<- any) error {
client := service.(*collector.Services).IAM
services := service.(*collector.Services)
client := services.IAM

accountSettingsDetail, err := describeAccountSettingsDetail(ctx, client)
accountSettingsDetail, err := describeAccountSettingsDetail(ctx, client, services.AccessAnalyzer)
if err != nil {
log.CtxLogger(ctx).Warn("describeAccountSettingsDetail error", zap.Error(err))
return err
Expand All @@ -68,7 +73,7 @@ func GetAccountSettingsDetail(ctx context.Context, service schema.ServiceInterfa
return nil
}

func describeAccountSettingsDetail(ctx context.Context, c *iam.Client) (AccountSettingsDetail, error) {
func describeAccountSettingsDetail(ctx context.Context, c *iam.Client, aaClient *accessanalyzerSvc.Client) (AccountSettingsDetail, error) {

passwordPolicy, err := getAccountPasswordPolicy(ctx, c)
if err != nil {
Expand All @@ -83,12 +88,30 @@ func describeAccountSettingsDetail(ctx context.Context, c *iam.Client) (AccountS
}

return AccountSettingsDetail{
PasswordPolicy: passwordPolicy,
AccountSummary: accountSummary,
AccountId: log.GetCloudAccountId(ctx),
PasswordPolicy: passwordPolicy,
AccountSummary: accountSummary,
AccountId: log.GetCloudAccountId(ctx),
AccessAnalyzers: listAccessAnalyzers(ctx, aaClient),
}, nil
}

func listAccessAnalyzers(ctx context.Context, client *accessanalyzerSvc.Client) []accessanalyzerTypes.AnalyzerSummary {
if client == nil {
return nil
}
var analyzers []accessanalyzerTypes.AnalyzerSummary
paginator := accessanalyzerSvc.NewListAnalyzersPaginator(client, &accessanalyzerSvc.ListAnalyzersInput{})
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
log.CtxLogger(ctx).Warn("list access analyzers error", zap.Error(err))
return analyzers
}
analyzers = append(analyzers, page.Analyzers...)
}
return analyzers
}

func getAccountSummary(ctx context.Context, c *iam.Client) (map[string]int32, error) {
input := iam.GetAccountSummaryInput{}
output, err := c.GetAccountSummary(ctx, &input)
Expand Down
Loading
Loading