internal/resources/providers/awslib/s3/provider.go (257 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
s3Client "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go-v2/service/s3control"
s3ControlTypes "github.com/aws/aws-sdk-go-v2/service/s3control/types"
"github.com/aws/smithy-go"
"github.com/elastic/cloudbeat/internal/infra/clog"
"github.com/elastic/cloudbeat/internal/resources/fetching"
"github.com/elastic/cloudbeat/internal/resources/providers/awslib"
)
const (
EncryptionNotFoundCode = "ServerSideEncryptionConfigurationNotFoundError"
PolicyNotFoundCode = "NoSuchBucketPolicy"
NoEncryptionMessage = "NoEncryption"
NoPublicAccessBlockConfigurationCode = "NoSuchPublicAccessBlockConfiguration"
)
func NewProvider(ctx context.Context, log *clog.Logger, cfg aws.Config, factory awslib.CrossRegionFactory[Client], accountId string) *Provider {
f := func(cfg aws.Config) Client {
return s3Client.NewFromConfig(cfg)
}
m := factory.NewMultiRegionClients(ctx, awslib.AllRegionSelector(), cfg, f, log)
controlClient := s3control.NewFromConfig(cfg)
return &Provider{
log: log,
clients: m.GetMultiRegionsClientMap(),
controlClient: controlClient,
accountId: accountId,
}
}
func (p Provider) DescribeBuckets(ctx context.Context) ([]awslib.AwsResource, error) {
defaultClient, err := awslib.GetDefaultClient(p.clients)
if err != nil {
return nil, fmt.Errorf("could not select default region client: %w", err)
}
clientBuckets, err := defaultClient.ListBuckets(ctx, &s3Client.ListBucketsInput{})
if err != nil {
p.log.Errorf("Could not list s3 buckets: %v", err)
return nil, err
}
var result []awslib.AwsResource
if len(clientBuckets.Buckets) == 0 {
return result, nil
}
accountPublicAccessBlockConfig, accountPublicAccessBlockErr := p.getAccountPublicAccessBlock(ctx)
if accountPublicAccessBlockErr != nil {
p.log.Errorf("Could not get account public access block configuration. Err: %v", accountPublicAccessBlockErr)
}
bucketsRegionsMapping := p.getBucketsRegionMapping(ctx, clientBuckets.Buckets)
for region, buckets := range bucketsRegionsMapping {
for _, bucket := range buckets {
// Getting the bucket encryption, policy, versioning and public access block is not critical for the rest
// of the flow, so we should keep describing the bucket even if getting these objects fails.
sseAlgorithm, encryptionErr := p.getBucketEncryptionAlgorithm(ctx, bucket.Name, region)
if encryptionErr != nil {
p.log.Errorf("Could not get encryption for bucket %s. Error: %v", *bucket.Name, encryptionErr)
}
bucketPolicy, policyErr := p.GetBucketPolicy(ctx, bucket.Name, region)
if policyErr != nil {
p.log.Errorf("Could not get bucket policy for bucket %s. Error: %v", *bucket.Name, policyErr)
}
bucketVersioning, versioningErr := p.getBucketVersioning(ctx, bucket.Name, region)
if versioningErr != nil {
p.log.Errorf("Could not get bucket versioning for bucket %s. Err: %v", *bucket.Name, versioningErr)
}
publicAccessBlockConfiguration, publicAccessBlockErr := p.getPublicAccessBlock(ctx, bucket.Name, region)
if publicAccessBlockErr != nil {
p.log.Errorf("Could not get public access block configuration for bucket %s. Err: %v", *bucket.Name, publicAccessBlockErr)
}
result = append(result, BucketDescription{
Name: *bucket.Name,
SSEAlgorithm: sseAlgorithm,
BucketPolicy: bucketPolicy,
BucketVersioning: bucketVersioning,
PublicAccessBlockConfiguration: publicAccessBlockConfiguration,
AccountPublicAccessBlockConfiguration: accountPublicAccessBlockConfig,
Region: region,
})
}
}
return result, nil
}
func (p Provider) GetBucketACL(ctx context.Context, bucketName *string, region string) (*s3Client.GetBucketAclOutput, error) {
client, err := awslib.GetClient(®ion, p.clients)
if err != nil {
return nil, err
}
acl, err := client.GetBucketAcl(ctx, &s3Client.GetBucketAclInput{Bucket: bucketName})
if err != nil {
p.log.Debugf("Error getting bucket ACL: %s", err)
return nil, err
}
return acl, nil
}
func (p Provider) GetBucketPolicy(ctx context.Context, bucketName *string, region string) (BucketPolicy, error) {
client, err := awslib.GetClient(®ion, p.clients)
if err != nil {
return nil, err
}
rawPolicy, err := client.GetBucketPolicy(ctx, &s3Client.GetBucketPolicyInput{Bucket: bucketName})
if err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
if apiErr.ErrorCode() == PolicyNotFoundCode {
p.log.Debugf("Bucket policy for bucket %s does not exist", *bucketName)
return map[string]any{}, nil
}
}
return nil, err
}
var bucketPolicy BucketPolicy
jsonErr := json.Unmarshal([]byte(*rawPolicy.Policy), &bucketPolicy)
if jsonErr != nil {
return map[string]any{}, jsonErr
}
return bucketPolicy, nil
}
func (p Provider) GetBucketLogging(ctx context.Context, bucketName *string, region string) (Logging, error) {
client, err := awslib.GetClient(®ion, p.clients)
if err != nil {
return Logging{}, err
}
logging, err := client.GetBucketLogging(ctx, &s3Client.GetBucketLoggingInput{Bucket: bucketName})
if err != nil {
p.log.Debugf("Error getting bucket logging: %s", err)
return Logging{}, err
}
bucketLogging := Logging{}
if logging.LoggingEnabled != nil {
bucketLogging.Enabled = true
bucketLogging.TargetBucket = *logging.LoggingEnabled.TargetBucket
}
return bucketLogging, nil
}
func (p Provider) getBucketsRegionMapping(ctx context.Context, buckets []types.Bucket) map[string][]types.Bucket {
bucketsRegionMap := make(map[string][]types.Bucket, 0)
for _, clientBucket := range buckets {
region, regionErr := p.getBucketRegion(ctx, clientBucket.Name)
// If we could not get the Region for a bucket, additional API calls for resources will probably fail, we should
// not describe this bucket.
if regionErr != nil {
p.log.Errorf("Could not get bucket location for bucket %s. Not describing this bucket. Error: %v", *clientBucket.Name, regionErr)
continue
}
bucketsRegionMap[region] = append(bucketsRegionMap[region], clientBucket)
}
return bucketsRegionMap
}
func (p Provider) getBucketEncryptionAlgorithm(ctx context.Context, bucketName *string, region string) (*string, error) {
client, err := awslib.GetClient(®ion, p.clients)
if err != nil {
return nil, err
}
encryption, err := client.GetBucketEncryption(ctx, &s3Client.GetBucketEncryptionInput{Bucket: bucketName})
if err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
if apiErr.ErrorCode() == EncryptionNotFoundCode {
p.log.Debugf("Bucket encryption for bucket %s does not exist", *bucketName)
return aws.String(NoEncryptionMessage), nil
}
}
return nil, err
}
if len(encryption.ServerSideEncryptionConfiguration.Rules) == 0 {
return aws.String(NoEncryptionMessage), nil
}
sseAlgo := string(encryption.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault.SSEAlgorithm)
return &sseAlgo, nil
}
func (p Provider) getBucketRegion(ctx context.Context, bucketName *string) (string, error) {
defaultClient, err := awslib.GetDefaultClient(p.clients)
if err != nil {
return "", fmt.Errorf("could not select default region client: %w", err)
}
location, err := defaultClient.GetBucketLocation(ctx, &s3Client.GetBucketLocationInput{Bucket: bucketName})
if err != nil {
return "", err
}
region := string(location.LocationConstraint)
// Region us-east-1 have a LocationConstraint of null...
if region == "" {
region = awslib.DefaultRegion
// ...but check if it's not the AWS GovCloud partition
if _, ok := p.clients[awslib.DefaultRegion]; !ok {
region = awslib.DefaultGovRegion
}
}
return region, nil
}
func (p Provider) getBucketVersioning(ctx context.Context, bucketName *string, region string) (*BucketVersioning, error) {
bucketVersioning := &BucketVersioning{false, false}
client, err := awslib.GetClient(®ion, p.clients)
if err != nil {
return nil, err
}
bucketVersioningResponse, err := client.GetBucketVersioning(ctx, &s3Client.GetBucketVersioningInput{Bucket: bucketName})
if err != nil {
return nil, err
}
if bucketVersioningResponse.Status == types.BucketVersioningStatusEnabled {
bucketVersioning.Enabled = true
}
if bucketVersioningResponse.MFADelete == types.MFADeleteStatusEnabled {
bucketVersioning.MfaDelete = true
}
return bucketVersioning, nil
}
func (p Provider) getAccountPublicAccessBlock(ctx context.Context) (*s3ControlTypes.PublicAccessBlockConfiguration, error) {
publicAccessBlock, err := p.controlClient.GetPublicAccessBlock(ctx, &s3control.GetPublicAccessBlockInput{AccountId: &p.accountId})
if err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
if apiErr.ErrorCode() == NoPublicAccessBlockConfigurationCode {
p.log.Debugf("Account public access block for account %s does not exist", p.accountId)
return nil, nil
}
}
return nil, err
}
if publicAccessBlock.PublicAccessBlockConfiguration == nil {
return nil, errors.New("account public access block configuration is null")
}
return publicAccessBlock.PublicAccessBlockConfiguration, nil
}
func (p Provider) getPublicAccessBlock(ctx context.Context, bucketName *string, region string) (*types.PublicAccessBlockConfiguration, error) {
client, err := awslib.GetClient(®ion, p.clients)
if err != nil {
return nil, err
}
publicAccessBlock, err := client.GetPublicAccessBlock(ctx, &s3Client.GetPublicAccessBlockInput{Bucket: bucketName})
if err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
if apiErr.ErrorCode() == NoPublicAccessBlockConfigurationCode {
p.log.Debugf("Bucket public access block for bucket %s does not exist", *bucketName)
return nil, nil
}
}
return nil, err
}
if publicAccessBlock.PublicAccessBlockConfiguration == nil {
return nil, errors.New("public access block configuration is null")
}
return publicAccessBlock.PublicAccessBlockConfiguration, nil
}
func (b BucketDescription) GetResourceArn() string {
return fmt.Sprintf("arn:aws:s3:::%s", b.Name)
}
func (b BucketDescription) GetResourceName() string {
return b.Name
}
func (b BucketDescription) GetResourceType() string {
return fetching.S3Type
}
func (b BucketDescription) GetRegion() string {
return b.Region
}