######################################################################################################################
#  Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.                                           #
#                                                                                                                    #
#  Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance    #
#  with the License. A copy of the License is located at                                                             #
#                                                                                                                    #
#      http://www.apache.org/licenses/LICENSE-2.0                                                                    #
#                                                                                                                    #
#  or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES #
#  OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions    #
#  and limitations under the License.                                                                                #
######################################################################################################################

import boto3
import botocore
import json
import logging
import math
import time
import datetime
import requests
from urllib.request import Request, urlopen
from os import environ
from botocore.config import Config
from backoff import on_exception, expo

logging.getLogger().debug('Loading function')

#======================================================================================================================
# Constants
#======================================================================================================================
API_CALL_NUM_RETRIES = 5
LIST_LIMIT  = 50
BATCH_DELETE_LIMIT = 500
DELAY_BETWEEN_DELETES = 2
RULE_SUFIX_RATE_BASED = "-HTTP Flood Rule"

waf_client = boto3.client(environ['API_TYPE'], config=Config(retries={'max_attempts': API_CALL_NUM_RETRIES}))

#======================================================================================================================
# Configure Access Log Bucket
#======================================================================================================================
#----------------------------------------------------------------------------------------------------------------------
# Create a bucket (if not exist) and configure an event to call Log Parser lambda funcion when new Access log file is
# created (and stored on this S3 bucket).
#
# This function can raise exception if:
# 01. A empty bucket name is used
# 02. The bucket already exists and was created in a account that you cant access
# 03. The bucket already exists and was created in a different region.
#     You can't trigger log parser lambda function from another region.
#
# All those requirements are pre-verified by helper function.
#----------------------------------------------------------------------------------------------------------------------
def configure_s3_bucket(region, bucket_name):
    logging.getLogger().debug("[configure_s3_bucket] Start")

    if bucket_name.strip() == "":
        raise Exception('Failed to configure access log bucket. Name cannot be empty!')

    #------------------------------------------------------------------------------------------------------------------
    # Create the S3 bucket (if not exist)
    #------------------------------------------------------------------------------------------------------------------
    s3_client = boto3.client('s3')
    try:
        s3_client.head_bucket(Bucket=bucket_name)
    except botocore.exceptions.ClientError as e:
        # If a client error is thrown, then check that it was a 404 error.
        # If it was a 404 error, then the bucket does not exist.
        error_code = int(e.response['Error']['Code'])
        if error_code == 404:
            if region == 'us-east-1':
                s3_client.create_bucket(Bucket=bucket_name, ACL='private')
            else:
                s3_client.create_bucket(Bucket=bucket_name, ACL='private', CreateBucketConfiguration={'LocationConstraint': region})

            # Begin waiting for the S3 bucket, mybucket, to exist
            s3_bucket_exists_waiter = s3_client.get_waiter('bucket_exists')
            s3_bucket_exists_waiter.wait(Bucket=bucket_name)

            # Enable server side encryption on the S3 bucket
            s3_client.put_bucket_encryption(
                Bucket=bucket_name,
                ServerSideEncryptionConfiguration={
                    'Rules': [
                        {
                            'ApplyServerSideEncryptionByDefault': {
                                'SSEAlgorithm': 'AES256'
                            }
                        },
                    ]
                }
            )
       
    logging.getLogger().debug("[configure_s3_bucket] End")

#----------------------------------------------------------------------------------------------------------------------
# Configure bucket event to call Log Parser whenever a new gz log or athena result file is added to the bucket;
# call partition s3 log function whenever athena log parser is chosen and a log file is added to the bucket
#----------------------------------------------------------------------------------------------------------------------
def add_s3_bucket_lambda_event(bucket_name, lambda_function_arn, lambda_log_partition_function_arn, lambda_parser, athena_parser):
    logging.getLogger().debug("[add_s3_bucket_lambda_event] Start")

    s3_client = boto3.client('s3')
    if lambda_function_arn is not None and (lambda_parser or athena_parser):
        notification_conf = s3_client.get_bucket_notification_configuration(Bucket=bucket_name)

        new_conf = {}
        new_conf['LambdaFunctionConfigurations'] = []

        if 'TopicConfigurations' in notification_conf:
            new_conf['TopicConfigurations'] = notification_conf['TopicConfigurations']

        if 'QueueConfigurations' in notification_conf:
            new_conf['QueueConfigurations'] = notification_conf['QueueConfigurations']

        if 'LambdaFunctionConfigurations' in notification_conf:
            for lfc in notification_conf['LambdaFunctionConfigurations']:
                for e in lfc['Events']:
                    if "ObjectCreated" in e:
                        if lfc['LambdaFunctionArn'] != lambda_function_arn and \
                           (lambda_log_partition_function_arn is None or
                            (lambda_log_partition_function_arn is not None and
                             lfc['LambdaFunctionArn'] != lambda_log_partition_function_arn)):
                            new_conf['LambdaFunctionConfigurations'].append(lfc)

        if lambda_parser:
            new_conf['LambdaFunctionConfigurations'].append({
                'Id': 'Call Log Parser',
                'LambdaFunctionArn': lambda_function_arn,
                'Events': ['s3:ObjectCreated:*'],
                'Filter': {'Key': {'FilterRules': [{'Name': 'suffix','Value': 'gz'}]}}
            })

        if athena_parser:
            new_conf['LambdaFunctionConfigurations'].append({
                'Id': 'Call Athena Result Parser',
                'LambdaFunctionArn': lambda_function_arn,
                'Events': ['s3:ObjectCreated:*'],
                'Filter': {'Key': {'FilterRules': [{'Name': 'prefix','Value': 'athena_results/'}, {'Name': 'suffix','Value': 'csv'}]}}
            })
            
        if lambda_log_partition_function_arn is not None:
            new_conf['LambdaFunctionConfigurations'].append({
                'Id': 'Call s3 log partition function',
                'LambdaFunctionArn': lambda_log_partition_function_arn,
                'Events': ['s3:ObjectCreated:*'],
                'Filter': {'Key': {'FilterRules': [{'Name': 'prefix','Value': 'AWSLogs/'}, {'Name': 'suffix','Value': 'gz'}]}}
            })
            
        logging.getLogger().info("[add_s3_bucket_lambda_event] LambdaFunctionConfigurations:\n %s"
                                 %(new_conf['LambdaFunctionConfigurations']))
                
        s3_client.put_bucket_notification_configuration(Bucket=bucket_name, NotificationConfiguration=new_conf)

    logging.getLogger().debug("[add_s3_bucket_lambda_event] End")

#----------------------------------------------------------------------------------------------------------------------
# Clean access log bucket event
#----------------------------------------------------------------------------------------------------------------------
def remove_s3_bucket_lambda_event(bucket_name, lambda_function_arn):
    if lambda_function_arn != None:
        logging.getLogger().debug("[remove_s3_bucket_lambda_event] Start")

        s3_client = boto3.client('s3')
        try:
            new_conf = {}
            notification_conf = s3_client.get_bucket_notification_configuration(Bucket=bucket_name)
            if 'TopicConfigurations' in notification_conf:
                new_conf['TopicConfigurations'] = notification_conf['TopicConfigurations']
            if 'QueueConfigurations' in notification_conf:
                new_conf['QueueConfigurations'] = notification_conf['QueueConfigurations']

            if 'LambdaFunctionConfigurations' in notification_conf:
                new_conf['LambdaFunctionConfigurations'] = []
                for lfc in notification_conf['LambdaFunctionConfigurations']:
                    if lfc['LambdaFunctionArn'] == lambda_function_arn:
                        continue #remove all references for Log Parser event
                    else:
                        new_conf['LambdaFunctionConfigurations'].append(lfc)

            s3_client.put_bucket_notification_configuration(Bucket=bucket_name, NotificationConfiguration=new_conf)

        except Exception as error:
            logging.getLogger().error("Failed to remove S3 Bucket lambda event. Check if the bucket still exists, you own it and has proper access policy.")
            logging.getLogger().error(str(error))

        logging.getLogger().debug("[remove_s3_bucket_lambda_event] End")


#======================================================================================================================
# Configure Rate Based Rule
#======================================================================================================================
@on_exception(expo, waf_client.exceptions.WAFStaleDataException, max_time=10)
def create_rate_based_rule(stack_name, request_threshold, metric_name_prefix):
    logging.getLogger().debug("[create_rate_based_rule] Start")

    rule_id = ""

    response = waf_client.create_rate_based_rule(
        Name = stack_name + RULE_SUFIX_RATE_BASED,
        MetricName = metric_name_prefix + 'HttpFloodRule',
        RateKey='IP',
        RateLimit=int(request_threshold.replace(",","")),
        ChangeToken=waf_client.get_change_token()['ChangeToken']
    )
    rule_id = response['Rule']['RuleId'].strip()

    logging.getLogger().debug("[create_rate_based_rule] End")
    return rule_id

@on_exception(expo, waf_client.exceptions.WAFStaleDataException, max_time=10)
def update_rate_based_rule(rule_id, request_threshold):
    logging.getLogger().debug("[update_rate_based_rule] Start")

    try:
        waf_client.update_rate_based_rule(
            RuleId=rule_id,
            Updates=[],
            RateLimit=int(request_threshold.replace(",","")),
            ChangeToken=waf_client.get_change_token()['ChangeToken']
        )

    except waf_client.exceptions.WAFNonexistentItemException:
        raise Exception("Rate based rule %s doesn't exist (already deleted or failed to create)"%rule_id)

    logging.getLogger().debug("[update_rate_based_rule] End")

@on_exception(expo, waf_client.exceptions.WAFStaleDataException, max_time=10)
def delete_rate_based_rule(rule_id):
    logging.getLogger().debug("[delete_rate_based_rule] Start")

    try:
        waf_client.delete_rate_based_rule(
            RuleId=rule_id,
            ChangeToken=waf_client.get_change_token()['ChangeToken']
        )

    except waf_client.exceptions.WAFNonexistentItemException:
        logging.getLogger().debug("[delete_rate_based_rule] Rate based rule %s doesn't exist (already deleted or failed to create)"%rule_id)

    logging.getLogger().debug("[delete_rate_based_rule] End")


#======================================================================================================================
# Configure Web ACl
#======================================================================================================================
@on_exception(expo, waf_client.exceptions.WAFStaleDataException, max_time=10)
def update_web_acl(web_acl_id, updates):
    logging.getLogger().debug("[update_web_acl] Start")

    if len(updates) > 0:
        waf_client.update_web_acl(
            WebACLId = web_acl_id,
            ChangeToken = waf_client.get_change_token()['ChangeToken'],
            Updates = updates
        )

    logging.getLogger().debug("[update_web_acl] End")

def process_rule_inclusion(priority, action, rule_type, protection_tag_name, rule_name, resource_properties, current_rules):
    update = None
    is_activated = True if (protection_tag_name == None or resource_properties[protection_tag_name] == "yes") else False
    rule_id = resource_properties[rule_name] if rule_name in resource_properties else None

    if is_activated and rule_id not in current_rules:
        update = {
            'Action': 'INSERT',
            'ActivatedRule': {
                'Priority': priority,
                'RuleId': rule_id,
                'Action': {'Type': action},
                'Type': rule_type
            }
        }
    return update

def process_rule_exclusion(protection_tag_name, rule_name, resource_properties, old_resource_properties, current_rules):
    update = None
    rule_id = old_resource_properties[rule_name] if rule_name in old_resource_properties else None
    rule_data = current_rules[rule_id] if rule_id in current_rules else None
    is_activated = resource_properties[protection_tag_name] == "yes"
    was_activated = old_resource_properties[protection_tag_name] == "yes"

    if was_activated and (not is_activated) and rule_id in current_rules:
        update = {
            'Action': 'DELETE',
            'ActivatedRule': {
                'Priority': rule_data['Priority'],
                'RuleId': rule_id,
                'Action': rule_data['Action'],
                'Type': rule_data['Type']
            }
        }

    return update

def configure_web_acl(resource_properties, old_resource_properties):
    logging.getLogger().debug("[configure_web_acl] Start")

    #------------------------------------------------------------------------------------------------------------------
    # Get Current Rule List
    #------------------------------------------------------------------------------------------------------------------
    current_rules = {}
    response = waf_client.get_web_acl(WebACLId=resource_properties['WAFWebACL'])
    for rule in response['WebACL']['Rules']:
        current_rules[rule['RuleId']] = {
            'Type': rule['Type'],
            'Priority': rule['Priority'],
            'Action': rule['Action'],
        }

    #------------------------------------------------------------------------------------------------------------------
    # For each protection, check if the rule needs to added to the web_acl
    #------------------------------------------------------------------------------------------------------------------
    updates = []
    updates.append(process_rule_inclusion(10, resource_properties['ActionWAFWhitelistRule'], 'REGULAR', None, 'WAFWhitelistRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(20, resource_properties['ActionWAFBlacklistRule'], 'REGULAR', None, 'WAFBlacklistRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(30, resource_properties['ActionWAFSqlInjectionRule'], 'REGULAR', 'ProtectionActivatedSqlInjection', 'WAFSqlInjectionRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(40, resource_properties['ActionWAFXssRule'], 'REGULAR', 'ProtectionActivatedCrossSiteScripting', 'WAFXssRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(50, resource_properties['ActionWAFHttpFloodRateBasedRule'], 'RATE_BASED', 'ProtectionActivatedHttpFloodRateBased', 'WAFHttpFloodRateBasedRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(55, resource_properties['ActionWAFHttpFloodRegularRule'], 'REGULAR', 'ProtectionActivatedHttpFloodRegular', 'WAFHttpFloodRegularRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(60, resource_properties['ActionWAFScannersProbesRule'], 'REGULAR', 'ProtectionActivatedScannersProbes', 'WAFScannersProbesRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(70, resource_properties['ActionWAFIPReputationListsRule'], 'REGULAR', 'ProtectionActivatedReputationLists', 'WAFIPReputationListsRule', resource_properties, current_rules))
    updates.append(process_rule_inclusion(90, resource_properties['ActionWAFBadBotRule'], 'REGULAR', 'ProtectionActivatedBadBot', 'WAFBadBotRule', resource_properties, current_rules))

    if old_resource_properties:
        updates.append(process_rule_exclusion('ProtectionActivatedSqlInjection', 'WAFSqlInjectionRule', resource_properties, old_resource_properties, current_rules))
        updates.append(process_rule_exclusion('ProtectionActivatedCrossSiteScripting', 'WAFXssRule', resource_properties, old_resource_properties, current_rules))
        updates.append(process_rule_exclusion('ProtectionActivatedHttpFloodRateBased', 'WAFHttpFloodRateBasedRule', resource_properties, old_resource_properties, current_rules))
        updates.append(process_rule_exclusion('ProtectionActivatedHttpFloodRegular', 'WAFHttpFloodRegularRule', resource_properties, old_resource_properties, current_rules))
        updates.append(process_rule_exclusion('ProtectionActivatedScannersProbes', 'WAFScannersProbesRule', resource_properties, old_resource_properties, current_rules))
        updates.append(process_rule_exclusion('ProtectionActivatedReputationLists', 'WAFIPReputationListsRule', resource_properties, old_resource_properties, current_rules))
        updates.append(process_rule_exclusion('ProtectionActivatedBadBot', 'WAFBadBotRule', resource_properties, old_resource_properties, current_rules))

    #------------------------------------------------------------------------------------------------------------------
    # Clean invalid update elements
    #------------------------------------------------------------------------------------------------------------------
    updates = [u for u in updates if u is not None]

    #------------------------------------------------------------------------------------------------------------------
    # Clean IP sets before delete them
    #------------------------------------------------------------------------------------------------------------------
    if old_resource_properties:
        rule_ids = [u['ActivatedRule']['RuleId'] for u in updates if u['Action'] == 'DELETE']
        if ('WAFScannersProbesRule' in old_resource_properties and old_resource_properties['WAFScannersProbesRule'] in rule_ids):
            clean_ip_set(old_resource_properties['WAFScannersProbesSet'])
        if ('WAFIPReputationListsRule' in old_resource_properties and old_resource_properties['WAFIPReputationListsRule'] in rule_ids):
            clean_ip_set(old_resource_properties['WAFReputationListsSet'])
        if ('WAFBadBotRule' in old_resource_properties and old_resource_properties['WAFBadBotRule'] in rule_ids):
            clean_ip_set(old_resource_properties['WAFBadBotSet'])

    #------------------------------------------------------------------------------------------------------------------
    # Update WebACL
    #------------------------------------------------------------------------------------------------------------------
    update_web_acl(resource_properties['WAFWebACL'], updates)

    logging.getLogger().debug("[configure_web_acl] End")

def clean_web_acl(web_acl_id):
    logging.getLogger().debug("[clean_web_acl] Start")

    #------------------------------------------------------------------------------------------------------------------
    # Get current rule list to be removed from the web ACL
    #------------------------------------------------------------------------------------------------------------------
    updates = []
    response = waf_client.get_web_acl(WebACLId=web_acl_id)
    for rule in response['WebACL']['Rules']:
        updates.append({
            'Action': 'DELETE',
            'ActivatedRule': {
                'Priority': rule['Priority'],
                'RuleId': rule['RuleId'],
                'Action': rule['Action'],
                'Type': rule['Type']
            }
        })

    #------------------------------------------------------------------------------------------------------------------
    # Update WebACL
    #------------------------------------------------------------------------------------------------------------------
    update_web_acl(web_acl_id, updates)

    logging.getLogger().debug("[clean_web_acl] End")

@on_exception(expo, waf_client.exceptions.WAFStaleDataException, max_time=10)
def waf_update_ip_set(ip_set_id, updates):
    logging.getLogger().debug('[waf_update_ip_set] Start')
    response = waf_client.update_ip_set(IPSetId=ip_set_id,
        ChangeToken=waf_client.get_change_token()['ChangeToken'],
        Updates=updates)
    logging.getLogger().debug('[waf_update_ip_set] End')
    return response

def clean_ip_set(ip_set_id):
    logging.getLogger().debug("[clean_ip_set] Clean IP Set %s"%ip_set_id)

    response = waf_client.get_ip_set(IPSetId=ip_set_id)
    while len(response['IPSet']['IPSetDescriptors']) > 0:
        counter = 0
        updates = []
        for ip in response['IPSet']['IPSetDescriptors']:
            updates.append({
                'Action': 'DELETE',
                'IPSetDescriptor': {
                    'Type': ip['Type'],
                    'Value': ip['Value']
                }
            })
            counter += 1
            if counter >= BATCH_DELETE_LIMIT:
                break

        logging.getLogger().debug("[clean_ip_set] Deleting %d IPs..."%len(updates))
        waf_update_ip_set(ip_set_id, updates)
        response = waf_client.get_ip_set(IPSetId=ip_set_id)
        if len(response['IPSet']['IPSetDescriptors']) > 0:
            logging.getLogger().debug('[clean_ip_set] Sleep %d sec befone next slot to avoid AWS WAF API throttling ...'%DELAY_BETWEEN_DELETES)
            time.sleep(DELAY_BETWEEN_DELETES)


#======================================================================================================================
# Configure AWS WAF Logs
#======================================================================================================================
def put_logging_configuration(web_acl_arn, delivery_stream_arn):
    logging.getLogger().debug("[put_logging_configuration] Start")

    waf_client.put_logging_configuration(
        LoggingConfiguration = {
            'ResourceArn': web_acl_arn,
            'LogDestinationConfigs': [delivery_stream_arn]
        }
    )

    logging.getLogger().debug("[put_logging_configuration] End")

def delete_logging_configuration(web_acl_arn):
    logging.getLogger().debug("[delete_logging_configuration] Start")

    waf_client.delete_logging_configuration(ResourceArn = web_acl_arn)

    logging.getLogger().debug("[delete_logging_configuration] End")

#======================================================================================================================
# Populate Reputation List
#======================================================================================================================
def populate_reputation_list(region, reputation_lists_parser_function, reputation_list_set):
    logging.getLogger().debug("[populate_reputation_list] Start")

    try:
        lambda_client = boto3.client('lambda')
        lambda_client.invoke(
            FunctionName=reputation_lists_parser_function.rsplit(":",1)[-1],
            Payload="""{
                  "lists": [
                    {
                        "url": "https://www.spamhaus.org/drop/drop.txt"
                    },
                    {
                        "url": "https://www.spamhaus.org/drop/edrop.txt"
                    },
                    {
                        "url": "https://check.torproject.org/exit-addresses",
                        "prefix": "ExitAddress "
                    },
                    {
                        "url": "https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt"
                    }
                  ],
                  "apiType":"%s",
                  "region":"%s",
                  "ipSetIds": [
                        "%s"
                  ]
                }"""%(environ['API_TYPE'], region, reputation_list_set)
        )

    except Exception as error:
        logging.getLogger().error("[create_stack] Failed to call IP Reputation List function")
        logging.getLogger().error(str(error))

    logging.getLogger().debug("[populate_reputation_list] End")

#======================================================================================================================
# Generate Log Parser Config File
#======================================================================================================================
def generate_app_log_parser_conf_file(stack_name, error_threshold, block_period, app_access_log_bucket, overwrite):
    logging.getLogger().debug("[generate_app_log_parser_conf_file] Start")

    local_file = '/tmp/' + stack_name + '-app_log_conf_LOCAL.json'
    remote_file = stack_name + '-app_log_conf.json'
    default_conf = {
        'general': {
            'errorThreshold': error_threshold,
            'blockPeriod': block_period,
            'errorCodes': ['400', '401', '403', '404', '405']
        },
        'uriList': {
        }
    }

    if not overwrite:
        try:
            s3 = boto3.resource('s3')
            file_obj = s3.Object(app_access_log_bucket, remote_file)
            file_content = file_obj.get()['Body'].read()
            remote_conf = json.loads(file_content)

            if 'general' in remote_conf and 'errorCodes' in remote_conf['general']:
                default_conf['general']['errorCodes'] = remote_conf['general']['errorCodes']

            if 'uriList' in remote_conf:
                default_conf['uriList'] = remote_conf['uriList']

        except Exception as e:
            logging.getLogger().debug("[generate_app_log_parser_conf_file] \tFailed to merge existing conf file data.")
            logging.getLogger().debug(e)

    with open(local_file, 'w') as outfile:
        json.dump(default_conf, outfile)

    s3_client = boto3.client('s3')
    s3_client.upload_file(local_file, app_access_log_bucket, remote_file, ExtraArgs={'ContentType': "application/json"})

    logging.getLogger().debug("[generate_app_log_parser_conf_file] End")

def generate_waf_log_parser_conf_file(stack_name, request_threshold, block_period, waf_access_log_bucket, overwrite):
    logging.getLogger().debug("[generate_waf_log_parser_conf_file] Start")

    local_file = '/tmp/' + stack_name + '-waf_log_conf_LOCAL.json'
    remote_file = stack_name + '-waf_log_conf.json'
    default_conf = {
        'general': {
            'requestThreshold': request_threshold,
            'blockPeriod': block_period,
            'ignoredSufixes': []
        },
        'uriList': {
        }
    }

    if not overwrite:
        try:
            s3 = boto3.resource('s3')
            file_obj = s3.Object(waf_access_log_bucket, remote_file)
            file_content = file_obj.get()['Body'].read()
            remote_conf = json.loads(file_content)

            if 'general' in remote_conf and 'ignoredSufixes' in remote_conf['general']:
                default_conf['general']['ignoredSufixes'] = remote_conf['general']['ignoredSufixes']

            if 'uriList' in remote_conf:
                default_conf['uriList'] = remote_conf['uriList']

        except Exception as e:
            logging.getLogger().debug("[generate_waf_log_parser_conf_file] \tFailed to merge existing conf file data.")
            logging.getLogger().debug(e)

    with open(local_file, 'w') as outfile:
        json.dump(default_conf, outfile)

    s3_client = boto3.client('s3')
    s3_client.upload_file(local_file, waf_access_log_bucket, remote_file, ExtraArgs={'ContentType': "application/json"})

    logging.getLogger().debug("[generate_waf_log_parser_conf_file] End")

#======================================================================================================================
# Add Athena Partitions
#======================================================================================================================
def add_athena_partitions(add_athena_partition_lambda_function, resource_type,
                          glue_database, access_log_bucket, glue_access_log_table,
                          glue_waf_log_table, waf_log_bucket, athena_work_group):
    logging.getLogger().info("[add_athena_partitions] Start")

    lambda_client = boto3.client('lambda')
    response = lambda_client.invoke(
        FunctionName=add_athena_partition_lambda_function.rsplit(":",1)[-1],
        Payload="""{
                "resourceType":"%s",
                "glueAccessLogsDatabase":"%s",
                "accessLogBucket":"%s",
                "glueAppAccessLogsTable":"%s",
                "glueWafAccessLogsTable":"%s",
                "wafLogBucket":"%s",
                "athenaWorkGroup":"%s"
            }"""%(resource_type, glue_database, access_log_bucket,
                  glue_access_log_table, glue_waf_log_table,
                  waf_log_bucket, athena_work_group)
    )
    logging.getLogger().info("[add_athena_partitions] Lambda invocation response:\n%s"%response)
    logging.getLogger().info("[add_athena_partitions] End")

#======================================================================================================================
# Auxiliary Functions
#======================================================================================================================
def send_response(event, context, responseStatus, responseData, resourceId, reason=None):
    logging.getLogger().debug("[send_response] Start")

    responseUrl = event['ResponseURL']
    cw_logs_url = "https://console.aws.amazon.com/cloudwatch/home?region=%s#logEventViewer:group=%s;stream=%s"%(context.invoked_function_arn.split(':')[3], context.log_group_name, context.log_stream_name)

    logging.getLogger().info(responseUrl)
    responseBody = {}
    responseBody['Status'] = responseStatus
    responseBody['Reason'] = reason or ('See the details in CloudWatch Logs: ' +  cw_logs_url)
    responseBody['PhysicalResourceId'] = resourceId
    responseBody['StackId'] = event['StackId']
    responseBody['RequestId'] = event['RequestId']
    responseBody['LogicalResourceId'] = event['LogicalResourceId']
    responseBody['NoEcho'] = False
    responseBody['Data'] = responseData

    json_responseBody = json.dumps(responseBody)
    logging.getLogger().debug("Response body:\n" + json_responseBody)

    headers = {
        'content-type' : '',
        'content-length' : str(len(json_responseBody))
    }

    try:
        response = requests.put(responseUrl,
                                data=json_responseBody,
                                headers=headers)
        logging.getLogger().debug("Status code: " + response.reason)

    except Exception as error:
        logging.getLogger().error("[send_response] Failed executing requests.put(..)")
        logging.getLogger().error(str(error))

    logging.getLogger().debug("[send_response] End")

def send_anonymous_usage_data(action_type, resource_properties):

    try:
        if 'SendAnonymousUsageData' not in resource_properties or resource_properties['SendAnonymousUsageData'].lower() != 'yes':
            return
        logging.getLogger().debug("[send_anonymous_usage_data] Start")

        usage_data = {
            "Solution": "SO0006",
            "UUID": resource_properties['UUID'],
            "TimeStamp": str(datetime.datetime.utcnow().isoformat()),
            "Data":
            {
                "Version": "2.3.0",
                "data_type" : "custom_resource",
                "region" : resource_properties['Region'],
                "action" : action_type,
                "sql_injection_protection": resource_properties['ActivateSqlInjectionProtectionParam'],
                "xss_scripting_protection": resource_properties['ActivateCrossSiteScriptingProtectionParam'],
                "http_flood_protection": resource_properties['ActivateHttpFloodProtectionParam'],
                "scanners_probes_protection": resource_properties['ActivateScannersProbesProtectionParam'],
                "reputation_lists_protection": resource_properties['ActivateReputationListsProtectionParam'],
                "bad_bot_protection": resource_properties['ActivateBadBotProtectionParam'],
                "request_threshold": resource_properties['RequestThreshold'],
                "error_threshold": resource_properties['ErrorThreshold'],
                "waf_block_period": resource_properties['WAFBlockPeriod']
            }
        }

        #--------------------------------------------------------------------------------------------------------------
        logging.getLogger().info("[send_anonymous_usage_data] Send Data")
        #--------------------------------------------------------------------------------------------------------------
        url = 'https://metrics.awssolutionsbuilder.com/generic'
        req = Request(url, method='POST', data=bytes(json.dumps(usage_data), encoding='utf8'), headers={'Content-Type': 'application/json'})
        rsp = urlopen(req)
        rspcode = rsp.getcode()
        logging.getLogger().debug('[send_anonymous_usage_data] Response Code: {}'.format(rspcode))
        logging.getLogger().debug("[send_anonymous_usage_data] End")

    except Exception as error:
        logging.getLogger().debug("[send_anonymous_usage_data] Failed to Send Data")
        logging.getLogger().debug(str(error))

#======================================================================================================================
# Lambda Entry Point
#======================================================================================================================
def lambda_handler(event, context):
    responseStatus = 'SUCCESS'
    reason = None
    responseData = {}
    resourceId = event['PhysicalResourceId'] if 'PhysicalResourceId' in event else event['LogicalResourceId']
    result = {
        'StatusCode': '200',
        'Body':  {'message': 'success'}
    }

    try:
        #------------------------------------------------------------------
        # Set Log Level
        #------------------------------------------------------------------
        global log_level
        log_level = str(environ['LOG_LEVEL'].upper())
        if log_level not in ['DEBUG', 'INFO','WARNING', 'ERROR','CRITICAL']:
            log_level = 'ERROR'
        logging.getLogger().setLevel(log_level)

        #----------------------------------------------------------
        # Read inputs parameters
        #----------------------------------------------------------
        logging.getLogger().info(event)
        request_type = event['RequestType'].upper() if ('RequestType' in event) else ""
        logging.getLogger().info(request_type)

        #----------------------------------------------------------
        # Process event
        #----------------------------------------------------------
        if event['ResourceType'] == "Custom::ConfigureAppAccessLogBucket":
            lambda_log_parser_function = event['ResourceProperties']['LogParser'] if 'LogParser' in event['ResourceProperties'] else None
            lambda_partition_s3_logs_function = event['ResourceProperties']['MoveS3LogsForPartition'] if 'MoveS3LogsForPartition' in event['ResourceProperties'] else None
            lambda_parser = True if event['ResourceProperties']['ScannersProbesLambdaLogParser'] == 'yes' else False
            athena_parser = True if event['ResourceProperties']['ScannersProbesAthenaLogParser'] == 'yes' else False

            if 'CREATE' in request_type:
                configure_s3_bucket(event['ResourceProperties']['Region'], event['ResourceProperties']['AppAccessLogBucket'])
                add_s3_bucket_lambda_event(event['ResourceProperties']['AppAccessLogBucket'],
                    lambda_log_parser_function,
                    lambda_partition_s3_logs_function,
                    lambda_parser,
                    athena_parser)

            elif 'UPDATE' in request_type:
                old_lambda_app_log_parser_function = event['OldResourceProperties']['LogParser'] if 'LogParser' in event['OldResourceProperties'] else None
                old_lambda_partition_s3_logs_function = event['OldResourceProperties']['MoveS3LogsForPartition'] if 'MoveS3LogsForPartition' in event['OldResourceProperties'] else None
                old_lambda_parser = True if event['OldResourceProperties']['ScannersProbesLambdaLogParser'] == 'yes' else False
                old_athena_parser = True if event['OldResourceProperties']['ScannersProbesAthenaLogParser'] == 'yes' else False

                if (event['OldResourceProperties']['AppAccessLogBucket'] != event['ResourceProperties']['AppAccessLogBucket'] or
                        old_lambda_app_log_parser_function != lambda_log_parser_function or
                        old_lambda_partition_s3_logs_function != lambda_partition_s3_logs_function or
                        old_lambda_parser != lambda_parser or
                        old_athena_parser != athena_parser):

                    remove_s3_bucket_lambda_event(event['OldResourceProperties']["AppAccessLogBucket"],
                        old_lambda_app_log_parser_function)
                    remove_s3_bucket_lambda_event(event['OldResourceProperties']["AppAccessLogBucket"],
                        old_lambda_partition_s3_logs_function)
                    add_s3_bucket_lambda_event(event['ResourceProperties']['AppAccessLogBucket'],
                        lambda_log_parser_function,
                        lambda_partition_s3_logs_function,
                        lambda_parser,
                        athena_parser)

            elif 'DELETE' in request_type:
                remove_s3_bucket_lambda_event(event['ResourceProperties']["AppAccessLogBucket"],
                    lambda_log_parser_function)
                remove_s3_bucket_lambda_event(event['ResourceProperties']["AppAccessLogBucket"],
                    lambda_partition_s3_logs_function)
                
        elif event['ResourceType'] == "Custom::ConfigureWafLogBucket":
            lambda_log_parser_function = event['ResourceProperties']['LogParser'] if 'LogParser' in event['ResourceProperties'] else None
            lambda_partition_s3_logs_function = None
            lambda_parser = True if event['ResourceProperties']['HttpFloodLambdaLogParser'] == 'yes' else False
            athena_parser = True if event['ResourceProperties']['HttpFloodAthenaLogParser'] == 'yes' else False

            if 'CREATE' in request_type:
                add_s3_bucket_lambda_event(event['ResourceProperties']['WafLogBucket'],
                    lambda_log_parser_function,
                    lambda_partition_s3_logs_function,
                    lambda_parser,
                    athena_parser)

            elif 'UPDATE' in request_type:
                old_lambda_app_log_parser_function = event['OldResourceProperties']['LogParser'] if 'LogParser' in event['OldResourceProperties'] else None
                old_lambda_parser = True if event['OldResourceProperties']['HttpFloodLambdaLogParser'] == 'yes' else False
                old_athena_parser = True if event['OldResourceProperties']['HttpFloodAthenaLogParser'] == 'yes' else False

                if (event['OldResourceProperties']['WafLogBucket'] != event['ResourceProperties']['WafLogBucket'] or
                        old_lambda_app_log_parser_function != lambda_log_parser_function or
                        old_lambda_parser != lambda_parser or
                        old_athena_parser != athena_parser):

                    remove_s3_bucket_lambda_event(event['OldResourceProperties']["WafLogBucket"],
                        old_lambda_app_log_parser_function)
                    add_s3_bucket_lambda_event(event['ResourceProperties']['WafLogBucket'],
                        lambda_log_parser_function,
                        lambda_partition_s3_logs_function,
                        lambda_parser,
                        athena_parser)

            elif 'DELETE' in request_type:
                remove_s3_bucket_lambda_event(event['ResourceProperties']["WafLogBucket"],
                    lambda_log_parser_function)

        elif event['ResourceType'] == "Custom::ConfigureRateBasedRule":
            if 'CREATE' in request_type:
                rbr_id = create_rate_based_rule(event['ResourceProperties']['StackName'], event['ResourceProperties']['RequestThreshold'], event['ResourceProperties']['MetricNamePrefix'])
                if (rbr_id != ""):
                    resourceId = rbr_id
                    responseData['RateBasedRuleId'] = rbr_id

            elif 'UPDATE' in request_type:
                responseData['RateBasedRuleId'] = event['PhysicalResourceId']
                if (event['OldResourceProperties']['RequestThreshold'] != event['ResourceProperties']['RequestThreshold']):
                    update_rate_based_rule(event['PhysicalResourceId'], event['ResourceProperties']['RequestThreshold'])

            elif 'DELETE' in request_type:
                delete_rate_based_rule(event['PhysicalResourceId'])

        elif event['ResourceType'] == "Custom::ConfigureWebAcl":
            if 'CREATE' in request_type:
                configure_web_acl(event['ResourceProperties'], None)

            elif 'UPDATE' in request_type:
                configure_web_acl(event['ResourceProperties'], event['OldResourceProperties'])

            elif 'DELETE' in request_type:
                clean_web_acl(event['ResourceProperties']['WAFWebACL'])
                clean_ip_set(event['ResourceProperties']['WAFWhitelistSet'])
                clean_ip_set(event['ResourceProperties']['WAFBlacklistSet'])
                if 'WAFScannersProbesSet' in event['ResourceProperties']:
                    clean_ip_set(event['ResourceProperties']['WAFScannersProbesSet'])
                if 'WAFReputationListsSet' in event['ResourceProperties']:
                    clean_ip_set(event['ResourceProperties']['WAFReputationListsSet'])
                if 'WAFBadBotSet' in event['ResourceProperties']:
                    clean_ip_set(event['ResourceProperties']['WAFBadBotSet'])

            send_anonymous_usage_data(event['RequestType'], event['ResourceProperties'])

        elif event['ResourceType'] == "Custom::PopulateReputationList":
            if 'CREATE' in request_type or 'UPDATE' in request_type:
                populate_reputation_list(event['ResourceProperties']['Region'],
                    event['ResourceProperties']['ReputationListsParser'],
                    event['ResourceProperties']['WAFReputationListsSet'])

            # DELETE: do nothing

        elif event['ResourceType'] == "Custom::ConfigureAWSWAFLogs":
            if 'CREATE' in request_type:
                put_logging_configuration(event['ResourceProperties']['WAFWebACLArn'],
                    event['ResourceProperties']['DeliveryStreamArn'])

            elif 'UPDATE' in request_type:
                delete_logging_configuration(event['OldResourceProperties']['WAFWebACLArn'])
                put_logging_configuration(event['ResourceProperties']['WAFWebACLArn'],
                    event['ResourceProperties']['DeliveryStreamArn'])

            elif 'DELETE' in request_type:
                delete_logging_configuration(event['ResourceProperties']['WAFWebACLArn'])

        elif event['ResourceType'] == "Custom::GenerateAppLogParserConfFile":
            stack_name = event['ResourceProperties']['StackName']
            error_threshold = int(event['ResourceProperties']['ErrorThreshold'])
            block_period = int(event['ResourceProperties']['WAFBlockPeriod'])
            app_access_log_bucket = event['ResourceProperties']['AppAccessLogBucket']

            if 'CREATE' in request_type:
                generate_app_log_parser_conf_file(stack_name, error_threshold, block_period, app_access_log_bucket, True)
            elif 'UPDATE' in request_type:
                generate_app_log_parser_conf_file(stack_name, error_threshold, block_period, app_access_log_bucket, False)

            # DELETE: do nothing

        elif event['ResourceType'] == "Custom::GenerateWafLogParserConfFile":
            stack_name = event['ResourceProperties']['StackName']
            request_threshold = int(event['ResourceProperties']['RequestThreshold'])
            block_period = int(event['ResourceProperties']['WAFBlockPeriod'])
            waf_access_log_bucket = event['ResourceProperties']['WafAccessLogBucket']

            if 'CREATE' in request_type:
                generate_waf_log_parser_conf_file(stack_name, request_threshold, block_period, waf_access_log_bucket, True)
            elif 'UPDATE' in request_type:
                generate_waf_log_parser_conf_file(stack_name, request_threshold, block_period, waf_access_log_bucket, False)

            # DELETE: do nothing

        elif event['ResourceType'] == "Custom::AddAthenaPartitions":
            if 'CREATE' in request_type or 'UPDATE' in request_type:
                add_athena_partitions(
                    event['ResourceProperties']['AddAthenaPartitionsLambda'],
                    event['ResourceProperties']['ResourceType'],
                    event['ResourceProperties']['GlueAccessLogsDatabase'],
                    event['ResourceProperties']['AppAccessLogBucket'],
                    event['ResourceProperties']['GlueAppAccessLogsTable'],
                    event['ResourceProperties']['GlueWafAccessLogsTable'],
                    event['ResourceProperties']['WafLogBucket'],
                    event['ResourceProperties']['AthenaWorkGroup'])

            # DELETE: do nothing

    except Exception as error:
        logging.getLogger().error(error)
        responseStatus = 'FAILED'
        reason = str(error)
        result = {
            'statusCode': '500',
            'body':  {'message': reason}
        }

    finally:
        #------------------------------------------------------------------
        # Send Result
        #------------------------------------------------------------------
        if 'ResponseURL' in event:
            send_response(event, context, responseStatus, responseData, resourceId, reason)

        return json.dumps(result)