AWSTemplateFormatVersion: 2010-09-09 Description: | Provisions resources for Service Catalog auditing automation Parameters: OrganizationId: Description: | The AWS Organization ID is unique to your organization. Retrieve this value from Services, Management & Governance, and AWS Organizations. Type: String PrimaryRegion: Description: Primary region to deploy central audit resources such as the DynamoDB table, the EventBridge event bus, and the Athena table Type: String ResourceNamePrefix: Description: Prefix for naming all of the resources created by this CloudFormation template. You may leave the default value. Type: String Default: "service-catalog" S3BucketName: Description: Provide the name of the S3 bucket that has the lambda deployment packages. AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ Type: String S3KeyPrefix: Description: Provide the directory of the S3 bucket that has the lambda deployment packages. You may leave the default value. AllowedPattern: ^[0-9a-zA-Z-/_]*$ Default: lambda/service_catalog_audit/ Type: String Conditions: # Create central resources only in PrimaryRegion DeployPrimaryRegion: !Equals [!Ref "AWS::Region", !Ref PrimaryRegion] Resources: ######################################################### # Copy Lambda Function Code ######################################################### CopyZipsBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub copy-files-${AWS::AccountId}-${AWS::Region} BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true VersioningConfiguration: Status: Enabled CopyZipsBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref CopyZipsBucket PolicyDocument: Statement: - Action: "s3:*" Condition: Bool: "aws:SecureTransport": "false" Effect: Deny Principal: "*" Resource: - !Sub "arn:aws:s3:::${CopyZipsBucket}/*" - !Sub "arn:aws:s3:::${CopyZipsBucket}" Sid: AllowSSLRequestsOnly CopyZips: Type: Custom::CopyZips Properties: ServiceToken: !GetAtt CopyZipsFunction.Arn DestBucket: !Ref CopyZipsBucket SourceBucket: !Ref S3BucketName Prefix: !Ref S3KeyPrefix Objects: - service_catalog_audit.zip CopyZipsRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Path: / Policies: - PolicyName: lambda-copier PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - s3:GetObject Resource: - !Sub "arn:aws:s3:::${S3BucketName}/*" - Effect: Allow Action: - s3:PutObject - s3:DeleteObject - s3:DeleteObjectTagging - s3:DeleteObjectVersion - s3:DeleteObjectVersionTagging - s3:Get* - s3:List* Resource: - !Sub "arn:aws:s3:::${CopyZipsBucket}/*" - !Sub "arn:aws:s3:::${CopyZipsBucket}" CopyZipsFunction: Type: AWS::Lambda::Function Properties: Description: Copies objects from a source S3 bucket to a destination Handler: index.handler Runtime: python3.7 Role: !GetAtt "CopyZipsRole.Arn" Timeout: 240 Code: ZipFile: | import json import logging import threading import boto3 import cfnresponse def copy_objects(source_bucket, dest_bucket, prefix, objects): s3 = boto3.client('s3') for o in objects: key = prefix + o copy_source = { 'Bucket': source_bucket, 'Key': key } print(('copy_source: %s' % copy_source)) print(('dest_bucket = %s'%dest_bucket)) print(('key = %s' %key)) s3.copy_object(CopySource=copy_source, Bucket=dest_bucket, Key=key) def delete_objects(bucket, prefix, objects): s3 = boto3.client('s3') objects = {'Objects': [{'Key': prefix + o} for o in objects]} s3.delete_objects(Bucket=bucket, Delete=objects) def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') cfnresponse.send(event, context, cfnresponse.FAILED, {}, None) def handler(event, context): # make sure we send a failure to CloudFormation if the function # is going to timeout timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) timer.start() print(('Received event: %s' % json.dumps(event))) status = cfnresponse.SUCCESS try: source_bucket = event['ResourceProperties']['SourceBucket'] dest_bucket = event['ResourceProperties']['DestBucket'] prefix = event['ResourceProperties']['Prefix'] objects = event['ResourceProperties']['Objects'] if event['RequestType'] == 'Delete': delete_objects(dest_bucket, prefix, objects) else: copy_objects(source_bucket, dest_bucket, prefix, objects) except Exception as e: logging.error('Exception: %s' % e, exc_info=True) status = cfnresponse.FAILED finally: timer.cancel() cfnresponse.send(event, context, status, {}, None) ######################################################### # S3 ######################################################### AuditAthenaBucket: Type: AWS::S3::Bucket Condition: DeployPrimaryRegion Properties: BucketName: !Sub audit-athena-${AWS::AccountId}-${AWS::Region} BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true VersioningConfiguration: Status: Enabled ######################################################### # DynamoDB ######################################################### AuditTable: Type: AWS::DynamoDB::Table Condition: DeployPrimaryRegion Properties: AttributeDefinitions: - AttributeName: "provisionedProductId" AttributeType: "S" - AttributeName: "accountIdRegion" AttributeType: "S" KeySchema: - AttributeName: "provisionedProductId" KeyType: "HASH" - AttributeName: "accountIdRegion" KeyType: "RANGE" ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Sub "${ResourceNamePrefix}-db-table" ######################################################### # SQS ######################################################### AuditDLQ: Type: AWS::SQS::Queue Properties: QueueName: !Sub "${ResourceNamePrefix}-dlq" AuditDLQPolicy: Type: AWS::SQS::QueuePolicy Properties: Queues: - !Ref AuditDLQ PolicyDocument: Statement: - Action: - sqs:SendMessage - sqs:ReceiveMessage Effect: "Allow" Resource: !GetAtt AuditDLQ.Arn Principal: Service: - events.amazonaws.com Condition: StringEquals: aws:PrincipalOrgId: !Ref OrganizationId AuditLambdaSQSEventSourceMapping: Type: AWS::Lambda::EventSourceMapping DependsOn: - AuditLambdaServiceRolePolicy Properties: Enabled: true EventSourceArn: !GetAtt AuditDLQ.Arn FunctionName: !GetAtt AuditLambdaFunction.Arn ######################################################### # Lambda ######################################################### AuditLambdaServiceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${ResourceNamePrefix}-${AWS::Region}-LambdaServiceRole Description: !Sub ${ResourceNamePrefix}-${AWS::Region} Lambda service role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - "sts:AssumeRole" ManagedPolicyArns: - arn:aws:iam::aws:policy/AWSLambdaExecute Path: "/" AuditLambdaServiceRolePolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub ${ResourceNamePrefix}-${AWS::Region}-LambdaServiceRole-Policy Description: !Sub ${ResourceNamePrefix}-${AWS::Region} Lambda service role policy Path: / PolicyDocument: Version: "2012-10-17" Statement: - Sid: AccessDynamoDB Effect: Allow Action: - dynamodb:DeleteItem - dynamodb:GetItem - dynamodb:GetRecords - dynamodb:GetShardIterator - dynamodb:PutItem - dynamodb:UpdateItem Resource: - !Sub arn:aws:dynamodb:${PrimaryRegion}:${AWS::AccountId}:table/${ResourceNamePrefix}-db-table - Sid: AccessSQS Effect: Allow Action: - sqs:ReceiveMessage - sqs:ChangeMessageVisibility - sqs:GetQueueUrl - sqs:DeleteMessage - sqs:GetQueueAttributes Resource: - !GetAtt AuditDLQ.Arn Roles: - !Ref AuditLambdaServiceRole AuditLambdaFunction: Type: AWS::Lambda::Function DependsOn: CopyZips Properties: FunctionName: !Sub ${ResourceNamePrefix}-lambda Handler: service_catalog_audit.lambda_handler Role: !GetAtt AuditLambdaServiceRole.Arn Code: S3Bucket: !Ref CopyZipsBucket S3Key: !Sub ${S3KeyPrefix}service_catalog_audit.zip Runtime: python3.8 Timeout: 30 TracingConfig: Mode: Active Environment: Variables: AUDIT_TABLE: !Sub ${ResourceNamePrefix}-db-table SQS_DLQ: !Sub https://sqs.${AWS::Region}.amazonaws.com/${AWS::AccountId}/${ResourceNamePrefix}-dlq PRIMARY_REGION: !Ref PrimaryRegion ######################################################### # EventBridge ######################################################### AuditEventBus: Type: AWS::Events::EventBus Condition: DeployPrimaryRegion Properties: Name: !Sub "${ResourceNamePrefix}-bus" AuditEventBusLogGroup: Type: AWS::Logs::LogGroup Condition: DeployPrimaryRegion Properties: LogGroupName: !Sub "/aws/events/${ResourceNamePrefix}-bus-events" RetentionInDays: 7 AuditEventBusArchive: Type: AWS::Events::Archive Condition: DeployPrimaryRegion Properties: ArchiveName: !Sub "${ResourceNamePrefix}-bus-archive" Description: Archive for Service Catalog product audit events RetentionDays: 90 SourceArn: !GetAtt AuditEventBus.Arn AuditEventBusPolicy: Type: AWS::Events::EventBusPolicy Condition: DeployPrimaryRegion Properties: EventBusName: !Ref AuditEventBus StatementId: "AuditEventBusStatement" Principal: "*" Action: "events:PutEvents" Condition: Key: "aws:PrincipalOrgID" Type: "StringEquals" Value: !Ref OrganizationId AuditEventRule: Type: AWS::Events::Rule Condition: DeployPrimaryRegion Properties: Name: ServiceCatalogAuditHubEventRule Description: Service Catalog audit hub event rule to monitor product lifecycle EventBusName: !Ref AuditEventBus EventPattern: { "source": ["aws.servicecatalog"], "detail-type": ["AWS API Call via CloudTrail"], "detail": { "eventSource": ["servicecatalog.amazonaws.com"], "eventName": [ "ProvisionProduct", "TerminateProvisionedProduct", "UpdateProvisionedProduct", ], }, } State: "ENABLED" Targets: - Arn: !GetAtt AuditLambdaFunction.Arn Id: "AuditLambdaFunction" - Arn: !GetAtt AuditEventBusLogGroup.Arn Id: "AuditEventBusLogGroup" PermissionForAuditEventsToInvokeLambda: Type: AWS::Lambda::Permission Condition: DeployPrimaryRegion Properties: FunctionName: !Ref AuditLambdaFunction Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: !GetAtt AuditEventRule.Arn Outputs: HubAccountEventBusArn: Condition: DeployPrimaryRegion Description: Hub account EventBridge ARN Value: !GetAtt AuditEventBus.Arn HubAccountSqsDlqArn: Description: Hub account SQS dead-letter queue ARN Value: !GetAtt AuditDLQ.Arn AuditAthenaBucketName: Condition: DeployPrimaryRegion Description: Hub account Athena DynamoDB spill bucket name Value: !Ref AuditAthenaBucket