AWSTemplateFormatVersion: 2010-09-09
Description: SecurityGroup Outbound FQDN Allow Policy
Parameters:
  UpdateTime:
    Description: Please input Update Time
    Type: String
    Default: 2020/01/01-01
  AlowFqdnList:
    Description: Please input FQDN List
    Type: CommaDelimitedList
    Default: 'host1.example.jp,host2.example.jp'
  VpcId:
    Description: Please select VPC
    Type: 'AWS::EC2::VPC::Id'
  Protocol:
    Description: Please input Protocol
    Type: String
    Default: tcp
    AllowedValues:
      - tcp
      - udp
  FromPort:
    Description: Please input FromPort
    Type: Number
    Default: 80
  ToPort:
    Description: Please input ToPort
    Type: Number
    Default: 80
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: CloudFormation Configuration
        Parameters:
          - UpdateTime
      - Label:
          default: FQDN Configuration
        Parameters:
          - AlowFqdnList
      - Label:
          default: SecurityGroup Configuration
        Parameters:
          - VpcId
          - Protocol
          - FromPort
          - ToPort
Resources:
  LambdaBasicExecRole:
    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'
  LambdaFqdnRuleHelper:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import socket
          import sys
          import logging
          import hashlib
          import cfnresponse

          # 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 as e:
                  raise


          def handler(event, context):
              '''  Lambda Main
              '''
              try:
                  logger.info("Event RequestType: {}".format(event['RequestType']))
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                      return "Lambda Delete Success"
                  
                  event_protocol = event["ResourceProperties"]["Protocol"]
                  event_fromport = event["ResourceProperties"]["FromPort"]
                  event_toport = event["ResourceProperties"]["ToPort"]
                  event_fqdn = event["ResourceProperties"]["Fqdn"]

                  ips = []
                  response_data = {}
                  response_data["Rules"] = []
                  
                  logger.info("Get IP Address")
                  for host in event_fqdn:
                      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])
                          for ip in gethost[2]:
                              response_data["Rules"].append({
                                  "IpProtocol": event_protocol,
                                  "FromPort": event_fromport,
                                  "ToPort": event_toport,
                                  "Description": gethost[0],
                                  "CidrIp": ip + "/32"})

                  if len(ips) == 0:
                      # No IP Address
                      logger.error("No FQDN Address Record")
                      raise Exception("Error No FQDN Address Record")

                  ips.sort()
                  msg = ",".join(ips)
                  logger.debug("IP List: {}".format(msg))
                  hash = hashlib.md5(msg.encode()).hexdigest()
                  logger.info("IP List Hash(md5): {}".format(hash))
                  response_data["Hash"] = hash

                  logger.debug("Respons Data:\n{}".format(response_data))
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
                  logger.info("SecurityGroup Helper Lambda Success")
                  return "Lambda Success"
                  
              except Exception as e:
                  logger.error("SecurityGroup Helper Lambda Failed")
                  logger.error(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
                  
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt 
        - LambdaBasicExecRole
        - Arn
      Runtime: python3.7
      Timeout: 10
  FqdnRules:
    Type: 'Custom::FqdnRuleHelper'
    Version: 1
    Properties:
      ServiceToken: !GetAtt 
        - LambdaFqdnRuleHelper
        - Arn
      Protocol: !Ref Protocol
      FromPort: !Ref FromPort
      ToPort: !Ref ToPort
      Fqdn: !Ref AlowFqdnList
      DateTag: !Ref UpdateTime
  DnsSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: '-'
      VpcId: !Ref VpcId
      SecurityGroupEgress: !GetAtt 
        - FqdnRules
        - Rules
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 10.10.10.10/32
      Tags:
        - Key: FqdnIpHash
          Value: !GetAtt 
            - FqdnRules
            - Hash
        - Key: UpdateTime
          Value: !Ref UpdateTime
Outputs:
  UpdateTime:
    Description: SecurityGroup Update Time
    Value: !Ref UpdateTime
  FqdnIpHash:
    Description: FQDN IP List Hash
    Value: !GetAtt 
      - FqdnRules
      - Hash