AWSTemplateFormatVersion: 2010-09-09
Description: Check FQDN Allow Policy & Update CloudFormation
Parameters:
  SgStack:
    Description: Please input FQDN Security Group Stack
    Type: String
    Default: "CloudFormation Stacn Name"
Resources:
  LambdaCheckFqdnExecRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
        - 'arn:aws:iam::aws:policy/AWSLambdaFullAccess'
        - 'arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess'

  LambdaCheckFqdn:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import os
          import socket
          import hashlib
          import logging
          import datetime
          import json
          
          import boto3
          import botocore
          
          # Log Config
          logger = logging.getLogger(__name__)
          logger.setLevel(logging.INFO)
          #logger.setLevel(logging.DEBUG)
          
          
          def foward_lookup(domain):
              ''' Get IP Address
              '''
              try:
                  return socket.gethostbyname_ex(domain)
              except Exception:
                  raise
          
          
          def lambda_handler(event, context):
              ''' Lambda Main
              '''
              env_cfnstack = os.environ['STACK_NAME']
              env_updatelambda = os.environ['LAMBDA_UPDATE']
              logger.info("SecurityGroup CloudFormation Stack Name: {}".format(env_cfnstack))
          
              # Get CloudFormation Data
              logger.info("Get CloudFormation Data")
              cf = boto3.client('cloudformation')
          
              try:
                  cfn_responses = cf.describe_stacks(StackName=env_cfnstack)
              except botocore.exceptions.ClientError as e:
                  logger.error("Error: CloudFormation Data Get Faild")
                  raise
                  
              for cfnpmt_response in cfn_responses['Stacks'][0]['Parameters']:
                  logger.debug("{}".format(cfnpmt_response))
                  if cfnpmt_response.get('ParameterKey') == 'AlowFqdnList':
                      # Get CloudFormation Parameters FQDN List
                      str_fqdns = cfnpmt_response.get('ParameterValue')
                      cfn_fqdns = str_fqdns.split(",")
              logger.info("CloudFormation Parameters FQDN List: {}".format(cfn_fqdns))
              
              for cfnoutput_response in cfn_responses['Stacks'][0]['Outputs']:
                  logger.debug("{}".format(cfnoutput_response))
                  if cfnoutput_response.get('OutputKey') == 'FqdnIpHash':
                      # Get CloudFormation Output FqdnIpHash
                      cfn_hash = cfnoutput_response.get('OutputValue')
              logger.info("CloudFormation Output FqdnIpHash: {}".format(cfn_hash))
              
              logger.info("Get FQDN Address Record")
              ips = []
              
              for host in cfn_fqdns:
                  try:
                      gethost = foward_lookup(host)
                  except socket.gaierror as e:
                      logger.warn("Host: {}, Warning: {}".format(host, e))
                  else:
                      logger.info("Host: {}, IP: {}".format(gethost[0], gethost[2]))
                      ips.extend(gethost[2])
              
              if len(ips) == 0:
                  # No IP Address
                  logger.error("Error: No FQDN Address Record")
                  raise Exception("Error: No FQDN Address Record")
              
              ips.sort()
              msg = ",".join(ips)
              logger.debug("IP List: {}".format(msg))
              check_hash = hashlib.md5(msg.encode()).hexdigest()
              logger.info("IP List Hash(md5): {}".format(check_hash))
              
              try:
                  # Check FQDN IPaddress and SecurityGroup Allow Address
                  if check_hash == cfn_hash:
                      # No Update
                      logger.info("IP List hashes are equal")
                      return "Success: CloudFormation No Update"
              except NameError as e:
                  logger.error("No IP List hash Value")
                  return "Error: Check CloudFormation & Lamdba"
          
              # Update SecurityGroup Lambda Event Run
              input_event = {
                  "STACK_NAME": env_cfnstack
              }
              payload = json.dumps(input_event)
              try:
                  boto3.client('lambda').invoke(
                      FunctionName=env_updatelambda,
                      InvocationType='Event',
                      Payload=payload)
              except botocore.exceptions.ClientError as e:
                  logger.error("Error: Lambda invoke Faild: {}".format(e))
                  raise
              else:
                  logger.info("Lambda invoke: {}".format(env_updatelambda))
              
              return "Lambda Success"

      Handler: index.lambda_handler
      Environment:
        Variables:
          STACK_NAME: !Ref SgStack
          LAMBDA_UPDATE: !Ref LambdaUpdateCfn
      MemorySize: 128
      Role: !GetAtt 
        - LambdaCheckFqdnExecRole
        - Arn
      Runtime: python3.7
      Timeout: 30
      
  ScheduledRule:
    Type: AWS::Events::Rule
    Properties:
      Description: FQDN Check ScheduledRule
      ScheduleExpression: rate(60 minutes)
      State: "ENABLED"
      Targets: 
        -
          Arn: !GetAtt
            - LambdaCheckFqdn
            - Arn
          Id: "Id001"
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt
        - LambdaCheckFqdn
        - Arn
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt
        - ScheduledRule
        - Arn

  LambdaUpdateCfnExecRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
        - 'arn:aws:iam::aws:policy/AmazonEC2FullAccess'
        - 'arn:aws:iam::aws:policy/AWSLambdaFullAccess'
        - 'arn:aws:iam::aws:policy/AWSCloudFormationFullAccess'

  LambdaUpdateCfn:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import socket
          import hashlib
          import logging
          import datetime
          
          import boto3
          import botocore
          
          # Log Config
          logger = logging.getLogger(__name__)
          logger.setLevel(logging.INFO)
          #logger.setLevel(logging.DEBUG)
          
          
          def foward_lookup(domain):
              ''' Get IP Address
              '''
              try:
                  return socket.gethostbyname_ex(domain)
              except Exception:
                  raise
          
          
          def lambda_handler(event, context):
              ''' Lambda Main
              '''
              event_cfnstack = event['STACK_NAME']
              logger.info("SecurityGroup CloudFormation Stack Name: {}".format(event_cfnstack))
          
              # Update SecurityGroup
              cf = boto3.client('cloudformation')
              logger.info("Stack Update SecurityGroup FQDN Allow Policy")
          
              try:
                  cfn_responses = cf.describe_stacks(StackName=event_cfnstack)
              except botocore.exceptions.ClientError as e:
                  logger.error("Error: CloudFormation Data Get Faild")
                  raise
              
              for cfnoutput_response in cfn_responses['Stacks'][0]['Outputs']:
                  logger.debug("cloudformation describe_stacks: {}".format(cfnoutput_response))
                  if cfnoutput_response.get('OutputKey') == 'UpdateTime':
                      # Get CloudFormation Output UpdateTime
                      cfn_datetag = cfnoutput_response.get('OutputValue')
              logger.debug("CloudFormation Output Date Tag: {}".format(cfn_datetag))
              
              # Create Date Tag
              cfnoutput_day, cfn_no = cfn_datetag.split("-")
              today_str = datetime.date.today().strftime('%Y/%m/%d')
              
              if cfnoutput_day == today_str:
                  next_no = '{:0=2}'.format(int(cfn_no) + 1)
              else:
                  next_no = '01'
                  
              next_datatag = '{}-{}'.format(today_str, next_no)
              logger.debug("Next Date Tag: {}".format(next_datatag))
              
              update_params = {
                  'StackName': event_cfnstack,
                  'UsePreviousTemplate': True,
                  'Parameters': [
                      {
                          'ParameterKey': 'UpdateTime',
                          'ParameterValue': next_datatag
                      },
                      {
                          'ParameterKey': 'AlowFqdnList',
                          'UsePreviousValue': True
                      },
                      {
                          'ParameterKey': 'Protocol',
                          'UsePreviousValue': True
                      },
                      {
                          'ParameterKey': 'FromPort',
                          'UsePreviousValue': True
                      },
                      {
                          'ParameterKey': 'ToPort',
                          'UsePreviousValue': True
                      },
                      {
                          'ParameterKey': 'VpcId',
                          'UsePreviousValue': True
                      },         
                  ],
                  'Capabilities': [
                      'CAPABILITY_IAM'
                  ]
              }
          
              # CloudFormation Update
              logger.info("CloudFormation Stack Update Start")
              
              try:
                  cf.update_stack(**update_params)
                  logger.info("...waiting for stack to be ready...")
                  waiter = cf.get_waiter('stack_update_complete')
                  waiter.wait(
                      StackName=event_cfnstack,
                      WaiterConfig={
                          'Dalay': 30,
                          'MaxAttempts': 10
                      }
                  )
              except botocore.exceptions.ClientError as e:
                  logger.error("Error: CloudFormation Update Falid")
                  raise
              else:
                  logger.info("CloudFormation Stack Update Success: {}"
                              .format(next_datatag))
                              
              return "Success: CloudFormation Update"

      Handler: index.lambda_handler
      MemorySize: 128
      Role: !GetAtt 
        - LambdaUpdateCfnExecRole
        - Arn
      Runtime: python3.7
      Timeout: 300
        
        