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 }