github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/internal/adapters/cloud/aws/s3/s3_test.go (about)

     1  package s3
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/aws/aws-sdk-go-v2/aws"
     7  	s3api "github.com/aws/aws-sdk-go-v2/service/s3"
     8  	s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
     9  	"github.com/khulnasoft-lab/defsec/internal/adapters/cloud/aws/test"
    10  	"github.com/khulnasoft-lab/defsec/pkg/providers/aws/s3"
    11  	"github.com/khulnasoft-lab/defsec/pkg/state"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"testing"
    16  
    17  	aws2 "github.com/khulnasoft-lab/defsec/internal/adapters/cloud/aws"
    18  )
    19  
    20  type publicAccessBlock struct {
    21  	blockPublicAcls       bool
    22  	blockPublicPolicy     bool
    23  	ignorePublicAcls      bool
    24  	restrictPublicBuckets bool
    25  }
    26  
    27  type bucketDetails struct {
    28  	bucketName          string
    29  	acl                 string
    30  	encrypted           bool
    31  	loggingEnabled      bool
    32  	loggingTargetBucket string
    33  	versioningEnabled   bool
    34  	publicAccessBlock   *publicAccessBlock
    35  }
    36  
    37  func Test_S3BucketACLs(t *testing.T) {
    38  
    39  	tests := []struct {
    40  		name    string
    41  		details bucketDetails
    42  	}{
    43  		{
    44  			name: "simple bucket with public-read acl",
    45  			details: bucketDetails{
    46  				bucketName: "test-bucket",
    47  				acl:        "public-read",
    48  				encrypted:  false,
    49  			},
    50  		},
    51  		{
    52  			name: "simple bucket with authenticated-read acl",
    53  			details: bucketDetails{
    54  				bucketName: "wide-open-bucket",
    55  				acl:        "authenticated-read",
    56  				encrypted:  false,
    57  			},
    58  		},
    59  		{
    60  			name: "simple bucket with public-read-write acl",
    61  			details: bucketDetails{
    62  				bucketName: "public-read-write-bucket",
    63  				acl:        "public-read-write",
    64  				encrypted:  false,
    65  			},
    66  		},
    67  		{
    68  			name: "simple bucket with private acl and encryption",
    69  			details: bucketDetails{
    70  				bucketName: "private-bucket",
    71  				acl:        "private",
    72  				encrypted:  true,
    73  			},
    74  		},
    75  	}
    76  
    77  	ra, stack, err := test.CreateLocalstackAdapter(t)
    78  	defer func() { _ = stack.Stop() }()
    79  	require.NoError(t, err)
    80  
    81  	for _, tt := range tests {
    82  		t.Run(tt.name, func(t *testing.T) {
    83  			bootstrapBucket(t, ra, tt.details)
    84  
    85  			testState := &state.State{}
    86  			s3Adapter := &adapter{}
    87  			err = s3Adapter.Adapt(ra, testState)
    88  			require.NoError(t, err)
    89  
    90  			require.Len(t, testState.AWS.S3.Buckets, 2)
    91  			var got s3.Bucket
    92  			for _, b := range testState.AWS.S3.Buckets {
    93  				if b.Name.Value() == tt.details.bucketName {
    94  					got = b
    95  					break
    96  				}
    97  			}
    98  
    99  			assert.Equal(t, tt.details.bucketName, got.Name.Value())
   100  			assert.Equal(t, tt.details.acl, got.ACL.Value())
   101  			assert.Equal(t, tt.details.encrypted, got.Encryption.Enabled.Value())
   102  			removeBucket(t, ra, tt.details)
   103  		})
   104  	}
   105  }
   106  
   107  func Test_S3BucketLogging(t *testing.T) {
   108  
   109  	tests := []struct {
   110  		name    string
   111  		details bucketDetails
   112  	}{
   113  		{
   114  			name: "simple bucket with no logging enabled",
   115  			details: bucketDetails{
   116  				bucketName:     "test-bucket",
   117  				acl:            "public-read",
   118  				loggingEnabled: false,
   119  			},
   120  		},
   121  		{
   122  			name: "simple bucket with logging enabled",
   123  			details: bucketDetails{
   124  				bucketName:          "test-bucket",
   125  				acl:                 "public-read",
   126  				loggingEnabled:      true,
   127  				loggingTargetBucket: "access-logs",
   128  			},
   129  		},
   130  	}
   131  
   132  	ra, stack, err := test.CreateLocalstackAdapter(t)
   133  	defer func() { _ = stack.Stop() }()
   134  	require.NoError(t, err)
   135  
   136  	for _, tt := range tests {
   137  		t.Run(tt.name, func(t *testing.T) {
   138  			bootstrapBucket(t, ra, tt.details)
   139  
   140  			testState := &state.State{}
   141  			s3Adapter := &adapter{}
   142  			err = s3Adapter.Adapt(ra, testState)
   143  			require.NoError(t, err)
   144  
   145  			assert.Len(t, testState.AWS.S3.Buckets, 2)
   146  			var got s3.Bucket
   147  			for _, b := range testState.AWS.S3.Buckets {
   148  				if b.Name.Value() == tt.details.bucketName {
   149  					got = b
   150  					break
   151  				}
   152  			}
   153  
   154  			assert.Equal(t, tt.details.bucketName, got.Name.Value())
   155  			if tt.details.loggingEnabled {
   156  				assert.Equal(t, tt.details.loggingTargetBucket, got.Logging.TargetBucket.Value())
   157  				assert.Equal(t, tt.details.loggingEnabled, got.Logging.Enabled.Value())
   158  			} else {
   159  				assert.False(t, got.Logging.Enabled.Value())
   160  			}
   161  			removeBucket(t, ra, tt.details)
   162  		})
   163  	}
   164  }
   165  
   166  func Test_S3BucketVersioning(t *testing.T) {
   167  
   168  	tests := []struct {
   169  		name    string
   170  		details bucketDetails
   171  	}{
   172  		{
   173  			name: "simple bucket with no versioning enabled",
   174  			details: bucketDetails{
   175  				bucketName:        "test-bucket-no-versioning",
   176  				acl:               "public-read",
   177  				versioningEnabled: false,
   178  			},
   179  		},
   180  		{
   181  			name: "simple bucket with versioning enabled",
   182  			details: bucketDetails{
   183  				bucketName:        "test-bucket-versioning",
   184  				acl:               "public-read",
   185  				versioningEnabled: true,
   186  			},
   187  		},
   188  	}
   189  
   190  	ra, stack, err := test.CreateLocalstackAdapter(t)
   191  	defer func() { _ = stack.Stop() }()
   192  	require.NoError(t, err)
   193  
   194  	for _, tt := range tests {
   195  		t.Run(tt.name, func(t *testing.T) {
   196  			bootstrapBucket(t, ra, tt.details)
   197  
   198  			testState := &state.State{}
   199  			s3Adapter := &adapter{}
   200  			err = s3Adapter.Adapt(ra, testState)
   201  			require.NoError(t, err)
   202  
   203  			assert.Len(t, testState.AWS.S3.Buckets, 2)
   204  			var got s3.Bucket
   205  			for _, b := range testState.AWS.S3.Buckets {
   206  				if b.Name.Value() == tt.details.bucketName {
   207  					got = b
   208  					break
   209  				}
   210  			}
   211  
   212  			assert.Equal(t, tt.details.bucketName, got.Name.Value())
   213  			if tt.details.loggingEnabled {
   214  				assert.Equal(t, tt.details.loggingTargetBucket, got.Logging.TargetBucket.Value())
   215  				assert.Equal(t, tt.details.loggingEnabled, got.Logging.Enabled.Value())
   216  			} else {
   217  				assert.False(t, got.Logging.Enabled.Value())
   218  			}
   219  			removeBucket(t, ra, tt.details)
   220  		})
   221  	}
   222  }
   223  
   224  func Test_S3PublicAccessBlock(t *testing.T) {
   225  
   226  	tests := []struct {
   227  		name    string
   228  		details bucketDetails
   229  	}{
   230  		{
   231  			name: "simple bucket with public access block that blocks public acls",
   232  			details: bucketDetails{
   233  				bucketName: "test-bucket-public-access-block",
   234  				publicAccessBlock: &publicAccessBlock{
   235  					blockPublicAcls: true,
   236  				},
   237  			},
   238  		},
   239  		{
   240  			name: "simple bucket with public access block that ignore public acls",
   241  			details: bucketDetails{
   242  				bucketName: "test-bucket-public-access-block",
   243  				publicAccessBlock: &publicAccessBlock{
   244  					ignorePublicAcls: true,
   245  				},
   246  			},
   247  		},
   248  		{
   249  			name: "simple bucket with public access block that restricts public buckets",
   250  			details: bucketDetails{
   251  				bucketName: "test-bucket-public-access-block",
   252  				publicAccessBlock: &publicAccessBlock{
   253  					restrictPublicBuckets: true,
   254  				},
   255  			},
   256  		},
   257  		{
   258  			name: "simple bucket with public access block that blocks public policies",
   259  			details: bucketDetails{
   260  				bucketName: "test-bucket-public-access-block",
   261  				publicAccessBlock: &publicAccessBlock{
   262  					blockPublicPolicy: true,
   263  				},
   264  			},
   265  		},
   266  	}
   267  
   268  	ra, stack, err := test.CreateLocalstackAdapter(t)
   269  	defer func() { _ = stack.Stop() }()
   270  	require.NoError(t, err)
   271  
   272  	for _, tt := range tests {
   273  		t.Run(tt.name, func(t *testing.T) {
   274  			bootstrapBucket(t, ra, tt.details)
   275  
   276  			testState := &state.State{}
   277  			s3Adapter := &adapter{}
   278  			err = s3Adapter.Adapt(ra, testState)
   279  			require.NoError(t, err)
   280  
   281  			assert.Len(t, testState.AWS.S3.Buckets, 2)
   282  			var got s3.Bucket
   283  			for _, b := range testState.AWS.S3.Buckets {
   284  				if b.Name.Value() == tt.details.bucketName {
   285  					got = b
   286  					break
   287  				}
   288  			}
   289  
   290  			assert.Equal(t, tt.details.bucketName, got.Name.Value())
   291  			if tt.details.publicAccessBlock != nil {
   292  				assert.Equal(t, tt.details.publicAccessBlock.blockPublicAcls, got.PublicAccessBlock.BlockPublicACLs.Value())
   293  				assert.Equal(t, tt.details.publicAccessBlock.ignorePublicAcls, got.PublicAccessBlock.IgnorePublicACLs.Value())
   294  				assert.Equal(t, tt.details.publicAccessBlock.restrictPublicBuckets, got.PublicAccessBlock.RestrictPublicBuckets.Value())
   295  				assert.Equal(t, tt.details.publicAccessBlock.blockPublicPolicy, got.PublicAccessBlock.BlockPublicPolicy.Value())
   296  			} else {
   297  				require.Nil(t, got.PublicAccessBlock)
   298  			}
   299  			removeBucket(t, ra, tt.details)
   300  		})
   301  	}
   302  }
   303  
   304  func bootstrapBucket(t *testing.T, ra *aws2.RootAdapter, spec bucketDetails) {
   305  
   306  	api := s3api.NewFromConfig(ra.SessionConfig())
   307  
   308  	_, err := api.CreateBucket(ra.Context(), &s3api.CreateBucketInput{
   309  		Bucket: aws.String(spec.bucketName),
   310  
   311  		ACL: aclToCannedACL(spec.acl),
   312  	})
   313  	require.NoError(t, err)
   314  
   315  	if spec.encrypted {
   316  		bootstrapBucketEncryption(t, api, ra.Context(), spec)
   317  	}
   318  
   319  	if spec.loggingEnabled {
   320  		bootstrapBucketLogging(t, api, ra.Context(), spec)
   321  	}
   322  
   323  	if spec.versioningEnabled {
   324  		bootstrapBucketVersioning(t, api, ra.Context(), spec)
   325  	}
   326  
   327  	if spec.publicAccessBlock != nil {
   328  		createPublicAccessBlock(t, api, ra.Context(), spec)
   329  	}
   330  }
   331  
   332  func bootstrapBucketEncryption(t *testing.T, api *s3api.Client, ctx context.Context, spec bucketDetails) {
   333  	_, err := api.PutBucketEncryption(ctx, &s3api.PutBucketEncryptionInput{
   334  		Bucket: aws.String(spec.bucketName),
   335  		ServerSideEncryptionConfiguration: &s3types.ServerSideEncryptionConfiguration{
   336  			Rules: []s3types.ServerSideEncryptionRule{
   337  				{
   338  					ApplyServerSideEncryptionByDefault: &s3types.ServerSideEncryptionByDefault{
   339  						SSEAlgorithm: s3types.ServerSideEncryptionAes256,
   340  					},
   341  					BucketKeyEnabled: true,
   342  				},
   343  			},
   344  		},
   345  	})
   346  	require.NoError(t, err)
   347  
   348  }
   349  
   350  func bootstrapBucketLogging(t *testing.T, api *s3api.Client, ctx context.Context, spec bucketDetails) {
   351  	_, err := api.PutBucketLogging(ctx, &s3api.PutBucketLoggingInput{
   352  		Bucket: aws.String(spec.bucketName),
   353  		BucketLoggingStatus: &s3types.BucketLoggingStatus{
   354  			LoggingEnabled: &s3types.LoggingEnabled{
   355  				TargetBucket: aws.String(spec.loggingTargetBucket),
   356  				TargetPrefix: aws.String("/logs"),
   357  				TargetGrants: []s3types.TargetGrant{
   358  					{
   359  						Permission: s3types.BucketLogsPermissionWrite,
   360  						Grantee: &s3types.Grantee{
   361  							Type: s3types.TypeGroup,
   362  							URI:  aws.String("http://acs.amazonaws.com/groups/s3/LogDelivery"),
   363  						},
   364  					},
   365  				},
   366  			},
   367  		},
   368  	})
   369  	require.NoError(t, err)
   370  }
   371  
   372  func bootstrapBucketVersioning(t *testing.T, api *s3api.Client, ctx context.Context, spec bucketDetails) {
   373  	_, err := api.PutBucketVersioning(ctx, &s3api.PutBucketVersioningInput{
   374  		Bucket: aws.String(spec.bucketName),
   375  		VersioningConfiguration: &s3types.VersioningConfiguration{
   376  			Status: s3types.BucketVersioningStatusEnabled,
   377  		},
   378  	})
   379  	require.NoError(t, err)
   380  }
   381  
   382  func createPublicAccessBlock(t *testing.T, api *s3api.Client, ctx context.Context, spec bucketDetails) {
   383  	_, err := api.PutPublicAccessBlock(ctx, &s3api.PutPublicAccessBlockInput{
   384  		Bucket: aws.String(spec.bucketName),
   385  		PublicAccessBlockConfiguration: &s3types.PublicAccessBlockConfiguration{
   386  			BlockPublicAcls:       spec.publicAccessBlock.blockPublicAcls,
   387  			IgnorePublicAcls:      spec.publicAccessBlock.ignorePublicAcls,
   388  			RestrictPublicBuckets: spec.publicAccessBlock.restrictPublicBuckets,
   389  			BlockPublicPolicy:     spec.publicAccessBlock.blockPublicPolicy,
   390  		},
   391  	})
   392  	require.NoError(t, err)
   393  }
   394  
   395  func aclToCannedACL(acl string) s3types.BucketCannedACL {
   396  	switch acl {
   397  	case "authenticated-read":
   398  		return s3types.BucketCannedACLAuthenticatedRead
   399  	case "public-read":
   400  		return s3types.BucketCannedACLPublicRead
   401  	case "public-read-write":
   402  		return s3types.BucketCannedACLPublicReadWrite
   403  	default:
   404  		return s3types.BucketCannedACLPrivate
   405  	}
   406  }
   407  
   408  func removeBucket(t *testing.T, ra *aws2.RootAdapter, spec bucketDetails) {
   409  
   410  	api := s3api.NewFromConfig(ra.SessionConfig())
   411  
   412  	_, err := api.DeleteBucket(ra.Context(), &s3api.DeleteBucketInput{
   413  		Bucket: aws.String(spec.bucketName),
   414  	})
   415  	require.NoError(t, err)
   416  }