AWSTemplateFormatVersion: '2010-09-09' Description: CloudFormation template for creating an S3 Event Notification for new objects that are created in a specified S3 bucket and send through SNS messages to an SQS Queue subscriber that you specify. Parameters: pBucketName: Type: String Description: The name of your centralized S3 bucket to configure for S3 event notifications whenever new objects are added. pAllowedAWSAccountId: Type: String Description: The AWS Account ID for the SQS queue subscriber which SNS will send the S3 event notification messages. AllowedPattern: (^$|^[0-9]{12}$) Conditions: cAllowedAWSAccountId: !Not - !Equals - !Ref pAllowedAWSAccountId - '' Resources: rS3EventNotificationLambdaFunction: Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: Access to CloudWatch Logs is granted to the Lambda execution role. - id: W89 reason: This function do not communicate with VPC resources. - id: W92 reason: Lambda does not need reserved concurrent executions. checkov: skip: - id: CKV_AWS_115 comment: Lambda does not need reserved concurrent executions. - id: CKV_AWS_116 comment: DLQ not needed. This Lambda function is triggered only by CloudFormation events. - id: CKV_AWS_117 comment: This function do not communicate with VPC resources. Properties: Handler: index.lambda_handler Role: !GetAtt rLambdaExecutionRole.Arn Runtime: python3.12 Timeout: 60 Code: ZipFile: | import json import boto3 import os import logging import cfnresponse from botocore.exceptions import ClientError logger = logging.getLogger() logger.setLevel(logging.INFO) S3 = boto3.client('s3') def create_event_notification(bucket_name, notif_type, notif_arn, events): notif_type = notif_type.lower() notif_key = {'sqs': ['QueueConfigurations', 'QueueArn', 'SQSQueue'], 'sns': ['TopicConfigurations', 'TopicArn', 'SNSQueue']} status = False try: if notif_type in notif_key: notif_config={ notif_key[notif_type][0] : [{ 'Id': notif_key[notif_type][2], notif_key[notif_type][1]: notif_arn, 'Events': events }] } S3.put_bucket_notification_configuration(Bucket=bucket_name, NotificationConfiguration=notif_config) status = True else: logger.warn(f'Notification type {notif_type} not supported') except ClientError as exe: logger.info(f'Exception: {exe}') return status def delete_event_notification(bucket_name): try: S3.put_bucket_notification_configuration( Bucket=bucket_name, NotificationConfiguration={} ) except ClientError as exe: logger.info(f'Exception: {exe}') return False return True def lambda_handler(event, context): logger.info(f'Event: {event}') response_data = {} bucket = event['ResourceProperties']['BucketName'] topic = event['ResourceProperties']['TopicArn'] events = ['s3:ObjectCreated:*'] logger.info(f'Bucket: {bucket}, Topic: {topic}') cu_events = ['Create', 'Update'] del_events = ['Delete'] request_type = event['RequestType'] try: if request_type in cu_events: result = create_event_notification(bucket, 'sns', topic, events) response_data = {'Status': 'SUCCESS', 'Output': result} logger.info(f'{request_type} completed: {response_data}') elif request_type in del_events: result = delete_event_notification(bucket) response_data = {'Status': 'SUCCESS', 'Output': result} logger.info(f'{request_type} completed: {response_data}') else: logger.info(f'Request type {request_type} not supported') except Exception as exe: logger.info(f'Exception: {exe}') response_data = {'Status': 'FAILED', 'Output': str(exe)} cfnresponse.send(event, context, response_data['Status'], response_data) rLambdaExecutionRole: Type: AWS::IAM::Role Metadata: cdk_nag: rules_to_suppress: - id: AwsSolutions-IAM5 reason: These permissions are required for lambda to create log group/stream(s) Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: LambdaS3ExecutionPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:PutBucketNotification - s3:GetBucketNotification Resource: !Sub arn:${AWS::Partition}:s3:::${pBucketName} - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:* rSNSTopicS3EventNotification: Type: AWS::SNS::Topic Properties: DisplayName: SNS Topic for new objects created in S3 Bucket KmsMasterKeyId: alias/aws/sns rSNSTopicPolicy: Type: AWS::SNS::TopicPolicy Properties: Topics: - !Ref rSNSTopicS3EventNotification PolicyDocument: Version: '2012-10-17' Statement: - Sid: AllowPublishToSQS Effect: Allow Principal: '*' Action: sns:Publish Resource: !Ref rSNSTopicS3EventNotification Condition: ArnEquals: aws:SourceArn: !Sub arn:${AWS::Partition}:s3:::${pBucketName} - Sid: AllowSQSSubscription Effect: Allow Principal: AWS: !If - cAllowedAWSAccountId - !Ref pAllowedAWSAccountId - !Ref AWS::AccountId Action: sns:Subscribe Resource: !Ref rSNSTopicS3EventNotification rS3NotificationCustomResource: Type: Custom::S3NotificationConfiguration Properties: ServiceToken: !GetAtt rS3EventNotificationLambdaFunction.Arn BucketName: !Ref pBucketName TopicArn: !Ref rSNSTopicS3EventNotification rCrossAccountIAMRole: Type: AWS::IAM::Role Metadata: cdk_nag: rules_to_suppress: - id: AwsSolutions-IAM5 reason: These permissions are required to list objects in the bucket. Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: !If - cAllowedAWSAccountId - !Ref pAllowedAWSAccountId - !Ref AWS::AccountId Action: - sts:AssumeRole Path: / Policies: - PolicyName: s3-permissions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:GetObject - s3:ListBucket Resource: - !Sub arn:${AWS::Partition}:s3:::${pBucketName} - !Sub arn:${AWS::Partition}:s3:::${pBucketName}/* Outputs: oSNSTopicARN: Description: ARN of the SNS topic that will receive the S3 event notifications Value: !Ref rSNSTopicS3EventNotification oCrossAccountIAMRoleName: Description: Cross Account IAM Role Name Value: !Ref rCrossAccountIAMRole oCrossAccountIAMRoleArn: Description: Cross Account IAM Role Arn Value: !GetAtt rCrossAccountIAMRole.Arn