Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
90 changes: 86 additions & 4 deletions aws/resource-trusts.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
type ResourceTrustsModule struct {
KMSClient *sdk.KMSClientInterface
APIGatewayClient *sdk.APIGatewayClientInterface
EC2Client *sdk.AWSEC2ClientInterface

// General configuration data
Caller sts.GetCallerIdentityOutput
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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--
Expand Down Expand Up @@ -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

Expand Down
61 changes: 60 additions & 1 deletion aws/resource-trusts_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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")
}
}
}
}
34 changes: 33 additions & 1 deletion aws/sdk/ec2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}
44 changes: 44 additions & 0 deletions aws/sdk/ec2_mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type MockedEC2Client2 struct {
describeSnapshots DescribeSnapshots
describeVolumes DescribeVolumes
describeImages DescribeImages
describeVpcEndpoints DescribeVpcEndpoints
}

type DescribeNetworkInterfaces struct {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions cli/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -1624,13 +1624,15 @@ 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
}
m := aws.ResourceTrustsModule{
KMSClient: &KMSClient,
APIGatewayClient: &APIGatewayClient,
EC2Client: &EC2Client,
Caller: *caller,
AWSProfileProvided: profile,
Goroutines: Goroutines,
Expand Down