diff --git a/aws/resource-trusts.go b/aws/resource-trusts.go index 4efcb1f6..c8305fc5 100644 --- a/aws/resource-trusts.go +++ b/aws/resource-trusts.go @@ -21,6 +21,7 @@ import ( type ResourceTrustsModule struct { KMSClient *sdk.KMSClientInterface APIGatewayClient *sdk.APIGatewayClientInterface + EC2Client *sdk.AWSEC2ClientInterface // General configuration data Caller sts.GetCallerIdentityOutput @@ -77,11 +78,11 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity fmt.Printf("[%s][%s] Enumerating Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account)) // if kms feature flag is enabled include kms in the supported services if includeKms { - fmt.Printf("[%s][%s] Supported Services: APIGateway, CodeBuild, ECR, EFS, Glue, KMS, Lambda, SecretsManager, S3, SNS, SQS\n", + fmt.Printf("[%s][%s] Supported Services: APIGateway, CodeBuild, ECR, EFS, Glue, KMS, Lambda, SecretsManager, S3, SNS, SQS, VpcEndpoint\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub)) } else { fmt.Printf("[%s][%s] Supported Services: APIGateway, CodeBuild, ECR, EFS, Glue, Lambda, SecretsManager, S3, SNS, "+ - "SQS (KMS requires --include-kms feature flag)\n", + "SQS, VpcEndpoint (KMS requires --include-kms feature flag)\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub)) } wg := new(sync.WaitGroup) @@ -197,7 +198,6 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity fmt.Printf("[%s][%s] No resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub)) } fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), m.output.CallingModule) - } func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2, includeKms bool) { @@ -309,6 +309,18 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap m.getAPIGatewayPoliciesPerRegion(r, wg, semaphore, dataReceiver) } } + + if m.EC2Client != nil { + res, err = servicemap.IsServiceInRegion("ec2", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + m.getVPCEndpointPoliciesPerRegion(r, wg, semaphore, dataReceiver) + } + } } func (m *ResourceTrustsModule) Receiver(receiver chan Resource2, receiverDone chan bool) { @@ -1030,6 +1042,76 @@ func (m *ResourceTrustsModule) getAPIGatewayPoliciesPerRegion(r string, wg *sync } } +func (m *ResourceTrustsModule) getVPCEndpointPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) { + defer func() { + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + wg.Done() + }() + semaphore <- struct{}{} + defer func() { <-semaphore }() + + vpcEndpoints, err := sdk.CachedEC2DescribeVpcEndpoints(*m.EC2Client, aws.ToString(m.Caller.Account), r) + if err != nil { + sharedLogger.Error(err.Error()) + return + } + + for _, vpcEndpoint := range vpcEndpoints { + var isPublic = "No" + var statementSummaryInEnglish string + var isInteresting = "No" + + if vpcEndpoint.PolicyDocument != nil && *vpcEndpoint.PolicyDocument != "" { + vpcEndpointPolicyJson := aws.ToString(vpcEndpoint.PolicyDocument) + vpcEndpointPolicy, err := policy.ParseJSONPolicy([]byte(vpcEndpointPolicyJson)) + if err != nil { + sharedLogger.Error(fmt.Errorf("parsing policy (%s) as JSON: %s", aws.ToString(vpcEndpoint.VpcEndpointId), err)) + m.CommandCounter.Error++ + continue + } + + if !vpcEndpointPolicy.IsEmpty() { + for i, statement := range vpcEndpointPolicy.Statement { + prefix := "" + if len(vpcEndpointPolicy.Statement) > 1 { + prefix = fmt.Sprintf("Statement %d says: ", i) + statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n" + } else { + statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account) + } + + statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n") + if isResourcePolicyInteresting(statementSummaryInEnglish) { + //magenta(statementSummaryInEnglish) + isInteresting = magenta("Yes") + } + + dataReceiver <- Resource2{ + AccountID: aws.ToString(m.Caller.Account), + ARN: fmt.Sprintf("arn:aws:ec2:%s:%s:vpc-endpoint/%s", r, aws.ToString(m.Caller.Account), aws.ToString(vpcEndpoint.VpcEndpointId)), + ResourcePolicySummary: statementSummaryInEnglish, + Public: isPublic, + Name: aws.ToString(vpcEndpoint.VpcEndpointId), + Region: r, + Interesting: isInteresting, + } + } + } + } else { + dataReceiver <- Resource2{ + AccountID: aws.ToString(m.Caller.Account), + ARN: fmt.Sprintf("arn:aws:ec2:%s:%s:vpc-endpoint/%s", r, aws.ToString(m.Caller.Account), aws.ToString(vpcEndpoint.VpcEndpointId)), + ResourcePolicySummary: statementSummaryInEnglish, + Public: isPublic, + Name: aws.ToString(vpcEndpoint.VpcEndpointId), + Region: r, + Interesting: isInteresting, + } + } + } +} + func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) { defer func() { m.CommandCounter.Executing-- @@ -1089,7 +1171,7 @@ func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sy } func isResourcePolicyInteresting(statementSummaryInEnglish string) bool { - // check if the statement has any of the following items, but make sure the check is case insensitive + // check if the statement has any of the following items, but make sure the check is case-insensitive // if it does, then return true // if it doesn't, then return false diff --git a/aws/resource-trusts_test.go b/aws/resource-trusts_test.go index cfb00c32..19a9eadc 100644 --- a/aws/resource-trusts_test.go +++ b/aws/resource-trusts_test.go @@ -1,10 +1,11 @@ package aws import ( + "testing" + "github.com/BishopFox/cloudfox/aws/sdk" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" - "testing" ) func TestIsResourcePolicyInteresting(t *testing.T) { @@ -62,6 +63,7 @@ func TestKMSResourceTrusts(t *testing.T) { testModule: ResourceTrustsModule{ KMSClient: &kmsClient, APIGatewayClient: nil, + EC2Client: nil, AWSRegions: []string{"us-west-2"}, Caller: sts.GetCallerIdentityOutput{ Account: aws.String("123456789012"), @@ -108,6 +110,7 @@ func TestAPIGatewayResourceTrusts(t *testing.T) { testModule: ResourceTrustsModule{ KMSClient: nil, APIGatewayClient: &apiGatewayClient, + EC2Client: nil, AWSRegions: []string{"us-west-2"}, Caller: sts.GetCallerIdentityOutput{ Account: aws.String("123456789012"), @@ -144,3 +147,59 @@ func TestAPIGatewayResourceTrusts(t *testing.T) { } } } + +func TestVpcEndpointResourceTrusts(t *testing.T) { + + mockedEC2Client := &sdk.MockedEC2Client2{} + var ec2Client sdk.AWSEC2ClientInterface = mockedEC2Client + + testCases := []struct { + outputDirectory string + verbosity int + testModule ResourceTrustsModule + expectedResult []Resource2 + }{ + { + outputDirectory: ".", + verbosity: 2, + testModule: ResourceTrustsModule{ + KMSClient: nil, + APIGatewayClient: nil, + EC2Client: &ec2Client, + AWSRegions: []string{"us-west-2"}, + Caller: sts.GetCallerIdentityOutput{ + Account: aws.String("123456789012"), + Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"), + }, + Goroutines: 30, + }, + expectedResult: []Resource2{ + { + Name: "vpce-1234567890abcdefg", + ARN: "vpce-1234567890abcdefg", + Public: "No", + }, + { + Name: "vpce-1234567890abcdefh", + ARN: "vpce-1234567890abcdefh", + Public: "No", + }, + }, + }, + } + + for _, tc := range testCases { + tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, false) + for index, expectedResource2 := range tc.expectedResult { + if expectedResource2.Name != tc.testModule.Resources2[index].Name { + t.Fatal("Resource name does not match expected value") + } + if expectedResource2.ARN != tc.testModule.Resources2[index].ARN { + t.Fatal("Resource ARN does not match expected value") + } + if expectedResource2.Public != tc.testModule.Resources2[index].Public { + t.Fatal("Resource Public does not match expected value") + } + } + } +} diff --git a/aws/sdk/ec2.go b/aws/sdk/ec2.go index 1deb5ae9..32476583 100644 --- a/aws/sdk/ec2.go +++ b/aws/sdk/ec2.go @@ -19,6 +19,7 @@ type AWSEC2ClientInterface interface { DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) DescribeImages(context.Context, *ec2.DescribeImagesInput, ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error) DescribeInstanceAttribute(context.Context, *ec2.DescribeInstanceAttributeInput, ...func(*ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error) + DescribeVpcEndpoints(context.Context, *ec2.DescribeVpcEndpointsInput, ...func(options *ec2.Options)) (*ec2.DescribeVpcEndpointsOutput, error) } func init() { @@ -27,7 +28,7 @@ func init() { gob.Register([]ec2Types.Snapshot{}) gob.Register([]ec2Types.Volume{}) gob.Register([]ec2Types.Image{}) - + gob.Register([]ec2Types.VpcEndpoint{}) } func CachedEC2DescribeInstances(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) { @@ -224,5 +225,36 @@ func CachedEC2DescribeImages(client AWSEC2ClientInterface, accountID string, reg internal.Cache.Set(cacheKey, Images, cache.DefaultExpiration) return Images, nil +} + +func CachedEC2DescribeVpcEndpoints(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.VpcEndpoint, error) { + var PaginationControl *string + var VpcEndpoints []ec2Types.VpcEndpoint + cacheKey := fmt.Sprintf("%s-ec2-DescribeVpcEndpoints-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]ec2Types.VpcEndpoint), nil + } + for { + DescribeVpcEndpoints, err := client.DescribeVpcEndpoints( + context.TODO(), + &(ec2.DescribeVpcEndpointsInput{ + NextToken: PaginationControl, + }), + func(o *ec2.Options) { + o.Region = region + }, + ) + if err != nil { + return VpcEndpoints, err + } + VpcEndpoints = append(VpcEndpoints, DescribeVpcEndpoints.VpcEndpoints...) + if DescribeVpcEndpoints.NextToken == nil { + break + } + PaginationControl = DescribeVpcEndpoints.NextToken + } + internal.Cache.Set(cacheKey, VpcEndpoints, cache.DefaultExpiration) + return VpcEndpoints, nil } diff --git a/aws/sdk/ec2_mocks.go b/aws/sdk/ec2_mocks.go index 51546cfd..d63165fa 100644 --- a/aws/sdk/ec2_mocks.go +++ b/aws/sdk/ec2_mocks.go @@ -25,6 +25,7 @@ type MockedEC2Client2 struct { describeSnapshots DescribeSnapshots describeVolumes DescribeVolumes describeImages DescribeImages + describeVpcEndpoints DescribeVpcEndpoints } type DescribeNetworkInterfaces struct { @@ -271,6 +272,9 @@ type DescribeInstanceAttribute struct { InstanceID string `json:"InstanceId"` } +type DescribeVpcEndpoints struct { +} + func (c *MockedEC2Client2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) { var nics []ec2types.NetworkInterface err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces) @@ -386,3 +390,43 @@ func (c *MockedEC2Client2) DescribeInstanceAttribute(ctx context.Context, input }, }, nil } + +func (c *MockedEC2Client2) DescribeVpcEndpoints(ctx context.Context, input *ec2.DescribeVpcEndpointsInput, f ...func(o *ec2.Options)) (*ec2.DescribeVpcEndpointsOutput, error) { + return &ec2.DescribeVpcEndpointsOutput{ + VpcEndpoints: []ec2types.VpcEndpoint{ + { + VpcEndpointId: aws.String("vpce-1234567890abcdefg"), + PolicyDocument: aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "*", + "Resource": "*" + } + ] + }`), + }, + { + VpcEndpointId: aws.String("vpce-1234567890abcdefh"), + PolicyDocument: aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "*", + "Resource": "*", + "Condition": { + "StringEquals": { + "aws:SourceVpce": "vpce-1234567890abcdefg" + } + } + } + ] + }`), + }, + }, + }, nil +} diff --git a/cli/aws.go b/cli/aws.go index e5f65cf3..8fac100a 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -1624,6 +1624,7 @@ func runResourceTrustsCommandWithProfile(cmd *cobra.Command, args []string, prof caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) var KMSClient sdk.KMSClientInterface = kms.NewFromConfig(AWSConfig) var APIGatewayClient sdk.APIGatewayClientInterface = apigateway.NewFromConfig(AWSConfig) + var EC2Client sdk.AWSEC2ClientInterface = ec2.NewFromConfig(AWSConfig) if err != nil { return @@ -1631,6 +1632,7 @@ func runResourceTrustsCommandWithProfile(cmd *cobra.Command, args []string, prof m := aws.ResourceTrustsModule{ KMSClient: &KMSClient, APIGatewayClient: &APIGatewayClient, + EC2Client: &EC2Client, Caller: *caller, AWSProfileProvided: profile, Goroutines: Goroutines,