text: package aws
  
  /**
   * Panther is a Cloud-Native SIEM for the Modern Security Team.
   * Copyright (C) 2020 Panther Labs Inc
   *
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Affero General Public License as
   * published by the Free Software Foundation, either version 3 of the
   * License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Affero General Public License for more details.
   *
   * You should have received a copy of the GNU Affero General Public License
   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
   */
  
  import (
  	"strings"
  
  	"github.com/aws/aws-sdk-go/aws"
  	"github.com/aws/aws-sdk-go/aws/arn"
  	"github.com/aws/aws-sdk-go/aws/awserr"
  	"github.com/aws/aws-sdk-go/aws/session"
  	"github.com/aws/aws-sdk-go/service/s3"
  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
  	"github.com/pkg/errors"
  	"go.uber.org/zap"
  
  	apimodels "github.com/panther-labs/panther/api/gateway/resources/models"
  	awsmodels "github.com/panther-labs/panther/internal/compliance/snapshot_poller/models/aws"
  	pollermodels "github.com/panther-labs/panther/internal/compliance/snapshot_poller/models/poller"
  	"github.com/panther-labs/panther/internal/compliance/snapshot_poller/pollers/utils"
  )
  
  var (
  	// S3ClientFunc is the function to initialize the S3 Client.
  	S3ClientFunc = setupS3Client
  )
  
  func setupS3Client(sess *session.Session, cfg *aws.Config) interface{} {
  	return s3.New(sess, cfg)
  }
  
  func getS3Client(pollerResourceInput *awsmodels.ResourcePollerInput, region string) (s3iface.S3API, error) {
  	client, err := getClient(pollerResourceInput, S3ClientFunc, "s3", region)
  	if err != nil {
  		return nil, err
  	}
  
  	return client.(s3iface.S3API), nil
  }
  
  // PollS3Bucket polls a single S3 Bucket resource
  func PollS3Bucket(
  	pollerResourceInput *awsmodels.ResourcePollerInput,
  	resourceARN arn.ARN,
  	scanRequest *pollermodels.ScanEntry,
  ) (interface{}, error) {
  
  	locationClient, err := getS3Client(pollerResourceInput, defaultRegion)
  	if err != nil {
  		return nil, err
  	}
  
  	// May return nil, nil if bucket no longer exists
  	region, err := getBucketLocation(locationClient, aws.String(resourceARN.Resource))
  	if err != nil || region == nil {
  		return nil, err
  	}
  
  	regionalClient, err := getS3Client(pollerResourceInput, *region)
  	if err != nil {
  		return nil, err
  	}
  
  	bucket, err := getBucket(regionalClient, resourceARN.Resource)
  	if err != nil {
  		return nil, err
  	}
  
  	// May return nil, nil if bucket no longer exists
  	snapshot, err := buildS3BucketSnapshot(regionalClient, bucket)
  	if err != nil || snapshot == nil {
  		return nil, err
  	}
  	snapshot.ResourceID = scanRequest.ResourceID
  	snapshot.AccountID = aws.String(pollerResourceInput.AuthSourceParsedARN.AccountID)
  	snapshot.Region = region
  	snapshot.ARN = scanRequest.ResourceID
  	return snapshot, nil
  }
  
  // getBucket returns a specific S3 bucket
  func getBucket(svc s3iface.S3API, bucketName string) (*s3.Bucket, error) {
  	// This horribly inefficient bucket lookup is required because (to my knowledge) there is no other
  	// way to get the bucket creation time.
  	buckets, err := listBuckets(svc)
  	if err != nil || buckets == nil {
  		return nil, errors.WithMessagef(err, "single resource scan for bucket %s", bucketName)
  	}
  	for _, bucket := range buckets.Buckets {
  		if aws.StringValue(bucket.Name) == bucketName {
  			return bucket, nil
  		}
  	}
  	return nil, nil
  }
  
  // getBucketLogging returns the logging policy for a given S3 bucket, and nil if one is not set
  func getBucketLogging(s3Svc s3iface.S3API, bucketName *string) (*s3.GetBucketLoggingOutput, error) {
  	out, err := s3Svc.GetBucketLogging(&s3.GetBucketLoggingInput{Bucket: bucketName})
  	if err != nil {
  		return nil, errors.Wrapf(err, "S3.GetBucketLogging: %s", aws.StringValue(bucketName))
  	}
  
  	return out, nil
  }
  
  // getObjectLockConfiguration returns the object lock configuration for an S3 bucket, if one exists
  func getObjectLockConfiguration(s3Svc s3iface.S3API, bucketName *string) (*s3.ObjectLockConfiguration, error) {
  	out, err := s3Svc.GetObjectLockConfiguration(&s3.GetObjectLockConfigurationInput{Bucket: bucketName})
  	if err != nil {
  		var awsErr awserr.Error
  		if errors.As(err, &awsErr) && awsErr.Code() == "ObjectLockConfigurationNotFoundError" {
  			zap.L().Debug("no object lock configuration found", zap.String("bucket", *bucketName))
  			return nil, nil
  		}
  		return nil, errors.Wrapf(err, "S3.GetObjectLockConfiguration: %s", aws.StringValue(bucketName))
  	}
  
  	return out.ObjectLockConfiguration, nil
  }
  
  // getBucketTagging returns the tags for a given S3 bucket
  func getBucketTagging(s3Svc s3iface.S3API, bucketName *string) ([]*s3.Tag, error) {
  	tags, err := s3Svc.GetBucketTagging(&s3.GetBucketTaggingInput{Bucket: bucketName})
  	if err != nil {
  		var awsErr awserr.Error
  		if errors.As(err, &awsErr) && awsErr.Code() == "NoSuchTagSet" {
  			zap.L().Debug("no tags found", zap.String("bucket", *bucketName))
  			return nil, nil
  		}
  		return nil, errors.Wrapf(err, "S3.GetBucketTagging: %s", aws.StringValue(bucketName))
  	}
  
  	return tags.TagSet, nil
  }
  
  // getBucketEncryption returns a list of server-side encryption settings for a given S3 bucket
  func getBucketEncryption(s3Svc s3iface.S3API, bucketName *string) ([]*s3.ServerSideEncryptionRule, error) {
  	out, err := s3Svc.GetBucketEncryption(&s3.GetBucketEncryptionInput{Bucket: bucketName})
  	if err != nil {
  		return nil, errors.Wrapf(err, "S3.GetBucketEncryption: %s", aws.StringValue(bucketName))
  	}
  
  	return out.ServerSideEncryptionConfiguration.Rules, nil
  }
  
  // getBucketPolicy returns the bucket policy of the given bucket as a JSON formatted string
  func getBucketPolicy(s3Svc s3iface.S3API, bucketName *string) (*string, error) {
  	out, err := s3Svc.GetBucketPolicy(&s3.GetBucketPolicyInput{Bucket: bucketName})
  	if err != nil {
  		return nil, errors.Wrapf(err, "S3.GetBucketPolicy: %s", aws.StringValue(bucketName))
  	}
  
  	return out.Policy, nil
  }
  
  // getBucketVersioning returns version information for a given s3 bucket
  func getBucketVersioning(s3Svc s3iface.S3API, bucketName *string) (*s3.GetBucketVersioningOutput, error) {
  	out, err := s3Svc.GetBucketVersioning(&s3.GetBucketVersioningInput{Bucket: bucketName})
  	if err != nil {
  		return nil, errors.Wrapf(err, "S3.GetBucketVersioning: %s", aws.StringValue(bucketName))
  	}
  
  	return out, nil
  }
  
  // getBucketLocation returns the region a bucket resides in
  func getBucketLocation(s3Svc s3iface.S3API, bucketName *string) (*string, error) {
  	out, err := s3Svc.GetBucketLocation(&s3.GetBucketLocationInput{Bucket: bucketName})
  	if err != nil {
  		var awsErr awserr.Error
  		if errors.As(err, &awsErr) && awsErr.Code() == s3.ErrCodeNoSuchBucket {
  			zap.L().Warn("tried to scan non-existent resource",
  				zap.String("resource", *bucketName),
  				zap.String("resourceType", awsmodels.S3BucketSchema))
  			return nil, nil
  		}
  		return nil, errors.Wrapf(err, "S3.GetBucketLocation: %s", aws.StringValue(bucketName))
  	}
  
  	// The get-bucket-location API call returns null for buckets in us-east-1.
  	if out.LocationConstraint == nil {
  		return aws.String("us-east-1"), nil
  	}
  
  	return out.LocationConstraint, nil
  }
  
  // getBucketLifecycle returns lifecycle configuration information set on a given bucket
  func getBucketLifecycleConfiguration(s3Svc s3iface.S3API, bucketName *string) ([]*s3.LifecycleRule, error) {
  	out, err := s3Svc.GetBucketLifecycleConfiguration(&s3.GetBucketLifecycleConfigurationInput{Bucket: bucketName})
  	if err != nil {
  		return nil, errors.Wrapf(err, "GetBucketLifecycleConfiguration: %s", aws.StringValue(bucketName))
  	}
  
  	return out.Rules, nil
  }
  
  // getBucketACL returns all ACLs for a given S3 bucket.
  func getBucketACL(s3Svc s3iface.S3API, bucketName *string) (*s3.GetBucketAclOutput, error) {
  	out, err := s3Svc.GetBucketAcl(&s3.GetBucketAclInput{Bucket: bucketName})
  	if err != nil {
  		return nil, errors.Wrapf(err, "S3.GetBucketACL: %s", aws.StringValue(bucketName))
  	}
  
  	return out, nil
  }
  
  // listS3Buckets returns a list of all S3 buckets in an account
  func listBuckets(s3Svc s3iface.S3API) (*s3.ListBucketsOutput, error) {
  	buckets, err := s3Svc.ListBuckets(&s3.ListBucketsInput{})
  	if err != nil {
  		return nil, errors.Wrap(err, "S3.ListBuckets")
  	}
  	return buckets, nil
  }
  
  // getPublicAccessBlock retrieves the PublicAccessBlock configuration for an Amazon S3 bucket
  func getPublicAccessBlock(s3Svc s3iface.S3API, bucketName *string) (*s3.PublicAccessBlockConfiguration, error) {
  	out, err := s3Svc.GetPublicAccessBlock(&s3.GetPublicAccessBlockInput{Bucket: bucketName})
  	if err != nil {
  		var awsErr awserr.Error
  		if errors.As(err, &awsErr) && awsErr.Code() == "NoSuchPublicAccessBlockConfiguration" {
  			zap.L().Debug(
  				"no public access block configuration found", zap.String("bucketName", *bucketName))
  			return nil, nil
  		}
  		return nil, errors.Wrapf(err, "S3.GetPublicAccessBlock: %s", aws.StringValue(bucketName))
  	}
  
  	return out.PublicAccessBlockConfiguration, nil
  }
  
  func buildS3BucketSnapshot(s3Svc s3iface.S3API, bucket *s3.Bucket) (*awsmodels.S3Bucket, error) {
  	if bucket == nil {
  		return nil, nil
  	}
  	s3Snapshot := &awsmodels.S3Bucket{
  		GenericResource: awsmodels.GenericResource{
  			TimeCreated:  utils.DateTimeFormat(*bucket.CreationDate),
  			ResourceType: aws.String(awsmodels.S3BucketSchema),
  		},
  		GenericAWSResource: awsmodels.GenericAWSResource{
  			Name: bucket.Name,
  		},
  	}
  
  	// Get the acls for each bucket
  	bucketAcls, err := getBucketACL(s3Svc, bucket.Name)
  	if err != nil {
  		return nil, err
  	}
  	s3Snapshot.Owner = bucketAcls.Owner
  	s3Snapshot.Grants = bucketAcls.Grants
  
  	tags, err := getBucketTagging(s3Svc, bucket.Name)
  	if err != nil {
  		return nil, err
  	}
  	if tags != nil {
  		s3Snapshot.Tags = utils.ParseTagSlice(tags)
  	}
  
  	objectLockConfiguration, err := getObjectLockConfiguration(s3Svc, bucket.Name)
  	if err != nil {
  		return nil, err
  	}
  	s3Snapshot.ObjectLockConfiguration = objectLockConfiguration
  
  	blockConfig, err := getPublicAccessBlock(s3Svc, bucket.Name)
  	if err != nil {
  		return nil, err
  	}
  	s3Snapshot.PublicAccessBlockConfiguration = blockConfig
  
  	// These api calls check on S3 bucket features which may have no value set.
  	// They return an error when that feature is not set, so we DEBUG log the error message here.
  	// TODO: Check all API calls below for expected errors (when configs do not exist)
  	//   and return an error if an unexpected one is returned (as above in getPublicAccessBlock).
  	loggingPolicy, err := getBucketLogging(s3Svc, bucket.Name)
  	if err != nil {
  		zap.L().Debug("S3.GetBucketLogging", zap.Error(err), zap.String("bucketName", *bucket.Name))
  	} else {
  		s3Snapshot.LoggingPolicy = loggingPolicy.LoggingEnabled
  	}
  
  	versioning, err := getBucketVersioning(s3Svc, bucket.Name)
  	if err != nil {
  		zap.L().Debug("S3.GetBucketVersioning", zap.Error(err), zap.String("bucketName", *bucket.Name))
  	} else {
  		s3Snapshot.Versioning = versioning.Status
  		s3Snapshot.MFADelete = versioning.MFADelete
  	}
  
  	lifecycleRules, err := getBucketLifecycleConfiguration(s3Svc, bucket.Name)
  	if err != nil {
  		zap.L().Debug("no bucket lifecycle configuration set", zap.Error(err), zap.String("bucketName", *bucket.Name))
  	} else {
  		s3Snapshot.LifecycleRules = lifecycleRules
  	}
  
  	encryption, err := getBucketEncryption(s3Svc, bucket.Name)
  	if err != nil {
  		zap.L().Debug("no bucket encryption set", zap.Error(err), zap.String("bucketName", *bucket.Name))
  	} else {
  		s3Snapshot.EncryptionRules = encryption
  	}
  
  	policy, err := getBucketPolicy(s3Svc, bucket.Name)
  	if err != nil {
  		zap.L().Debug("no bucket policy set", zap.Error(err), zap.String("bucketName", *bucket.Name))
  	} else {
  		s3Snapshot.Policy = policy
  	}
  
  	return s3Snapshot, nil
  }
  
  // PollS3Buckets gathers information on each S3 bucket for an AWS account.
  func PollS3Buckets(pollerInput *awsmodels.ResourcePollerInput) ([]*apimodels.AddResourceEntry, *string, error) {
  	zap.L().Debug("starting S3 Bucket resource poller")
  	s3Svc, err := getS3Client(pollerInput, *pollerInput.Region)
  	if err != nil {
  		return nil, nil, err
  	}
  
  	// Start with generating a list of all buckets
  	allBuckets, err := listBuckets(s3Svc)
  	if err != nil || len(allBuckets.Buckets) == 0 {
  		return nil, nil, errors.WithMessagef(err, "region: %s", *pollerInput.Region)
  	}
  
  	// For each bucket, determine its region, then only consider the bucket if it's region matches
  	// the requested region. May have timeout issues for accounts with lots of buckets, but the s3
  	// API is pretty limited in dealing with these things.
  	var buckets []*s3.Bucket
  	for _, bucket := range allBuckets.Buckets {
  		if bucket == nil {
  			zap.L().Debug("nil bucket returned by S3 list buckets")
  			continue
  		}
  		region, err := getBucketLocation(s3Svc, bucket.Name)
  		if err != nil {
  			return nil, nil, err
  		}
  		// This may occur if the bucket stopped existing between the call to list buckets and the
  		// call to get bucket location
  		if region == nil {
  			continue
  		}
  
  		if *region == *pollerInput.Region {
  			buckets = append(buckets, bucket)
  		}
  	}
  
  	var resources []*apimodels.AddResourceEntry
  	for _, bucket := range buckets {
  		s3BucketSnapshot, err := buildS3BucketSnapshot(s3Svc, bucket)
  		if err != nil {
  			return nil, nil, err
  		}
  		resourceID := strings.Join(
  			[]string{"arn", pollerInput.AuthSourceParsedARN.Partition, "s3::", *s3BucketSnapshot.Name},
  			":",
  		)
  
  		// Populate generic fields
  		s3BucketSnapshot.ResourceID = aws.String(resourceID)
  
  		// Populate AWS generic fields
  		s3BucketSnapshot.AccountID = aws.String(pollerInput.AuthSourceParsedARN.AccountID)
  		s3BucketSnapshot.ARN = aws.String(resourceID)
  		s3BucketSnapshot.Region = pollerInput.Region
  
  		resources = append(resources, &apimodels.AddResourceEntry{
  			Attributes:      s3BucketSnapshot,
  			ID:              apimodels.ResourceID(resourceID),
  			IntegrationID:   apimodels.IntegrationID(*pollerInput.IntegrationID),
  			IntegrationType: apimodels.IntegrationTypeAws,
  			Type:            awsmodels.S3BucketSchema,
  		})
  	}
  
  	// s3 buckets can never paginate
  	return resources, nil, nil
  }
  