# Centralized Logging Solution
#
# template for centralized-logging-solution
# this template will deploy sample log sources
#
# author: aws-solutions-builder@
AWSTemplateFormatVersion: 2010-09-09

Description: (SO0009d) - AWS Centralized Logging Solution, sample sources template (Version %%VERSION%%)

Parameters:
  # Log Streamer Function Arn
  LogStreamerArn:
    Description: Lambda Arn for Log Streamer function from primer template
    Type: String

  # VPC CIDR for proxy servers
  DemoVPCCidr:
    Description: CIDR block for VPC
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'
    Default: 10.250.0.0/16
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x

  # Subnet for proxy web server
  DemoSubnet:
    Description: IP address range for subnet
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'
    Default: 10.250.250.0/24
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x

  LatestAMIId:
    Description: >-
      Automatically selects the latest Amazon Linux AMI. Do not change this
      value
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: /aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: Network Configuration
      Parameters:
      - DemoVPCCidr
      - DemoSubnet
    ParameterLabels:
      DemoVPCCidr:
        default: VPC for Sample Log Sources
      DemoSubnet:
        default: Subnet for Sample Web Server
      LogStreamerArn:
        default: Lambda Arn for Log Streaming

Mappings:

  # Instance type for demo box
  EC2: 
    Instance: 
      Type: 't3.micro' 

  # CloudWatch logs pattern mapping
  FilterPatternLookup:
    Common:
      Pattern: '[host, ident, authuser, date, request, status, bytes, referrer, agent]'
    CloudTrail:
      Pattern: ''
    FlowLogs:
      Pattern: '[version, account_id, interface_id, srcaddr != "-", dstaddr != "-", srcport != "-", dstport != "-", protocol, packets, bytes, start, end, action, log_status]'
    Lambda:
      Pattern: '[timestamp=*Z, request_id="*-*", event]'
    SpaceDelimited:
      Pattern: '[]'
    Other:
      Pattern: ''

Resources:
  #
  # Demo VPC resources
  # [DemoVPC, PublicSubnet, InternetGateway, GatewayAttachment, PublicRtb, PublicRoute, SubnetRtbAssoc]
  #
  DemoVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Sub ${DemoVPCCidr}
      Tags:
      - Key: Name
        Value: centralized-logging-demo VPC

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Sub ${DemoVPC}
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      MapPublicIpOnLaunch: true
      CidrBlock: !Sub ${DemoSubnet}
      Tags:
      - Key: Name
        Value: centralized-logging-demo subnet
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W33
            reason: "This is where the test webserver is deployed for for a static page to demo the functionality of the solution by generating log for demo purposes. Hence requires a public IP"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties: {}

  GatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Sub ${DemoVPC}
      InternetGatewayId: !Sub ${InternetGateway}

  PublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Sub ${DemoVPC}

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: GatewayAttachment
    Properties:
      RouteTableId: !Sub ${PublicRtb}
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Sub ${InternetGateway}

  SubnetRtbAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Sub ${PublicSubnet}
      RouteTableId: !Sub ${PublicRtb}


  #
  # VPC flow log resources
  # [VPCFlowLogGroup, FlowlogsRole, VPCFlowLog, VPCFlowLogtoLambda]
  #
  VPCFlowLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 1

  FlowlogsRole:
    Type: AWS::IAM::Role 
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - vpc-flow-logs.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: LogRolePolicy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:DescribeLogGroups
            - logs:DescribeLogStreams
            - logs:PutLogEvents
            Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: "Push logs from various resources to cloudwatch logs. Hence open"

  VPCFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      DeliverLogsPermissionArn: !Sub ${FlowlogsRole.Arn}
      LogGroupName: !Sub ${VPCFlowLogGroup}
      ResourceId: !Sub ${DemoVPC}
      ResourceType: VPC
      TrafficType: ALL

  VPCFlowLogtoLambda:
    Type: AWS::Logs::SubscriptionFilter
    Properties:
      DestinationArn: !Sub ${LogStreamerArn}
      FilterPattern: !FindInMap [FilterPatternLookup, FlowLogs, Pattern]
      LogGroupName: !Sub ${VPCFlowLogGroup}

  S3LoggingBucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub aws-trailbucket-s3-access-logs-${AWS::AccountId}-${AWS::Region}
      AccessControl: LogDeliveryWrite
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      Tags:
        - Key: Name
          Value: 'AWS Centralized Logging Demo'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W35
            reason: "This S3 bucket is used as the destination for storing access logs"
          - id: W51
            reason: "Log delivery controlled by ACL, not bucket policy"

  #
  # CloudTrail resources
  # [TrailBucket, TrailBucketPolicy, TrailLogGroup, TrailLogGroupRole, TrailLogtoLambda]
  #
  TrailBucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      LoggingConfiguration:
        DestinationBucketName: !Ref S3LoggingBucket
        LogFilePrefix: access-logs
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      Tags:
        - Key: Name
          Value: 'AWS Centralized Logging Demo'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  TrailBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref TrailBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Principal:
            Service: cloudtrail.amazonaws.com
          Action: s3:GetBucketAcl
          Resource: !Sub arn:aws:s3:::${TrailBucket}
        - Effect: Allow
          Principal:
            Service: cloudtrail.amazonaws.com
          Action: s3:PutObject
          Resource: !Sub arn:aws:s3:::${TrailBucket}/AWSLogs/${AWS::AccountId}/*
          Condition:
            StringEquals:
              s3:x-amz-acl: bucket-owner-full-control

  TrailLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 1

  TrailLogGroupRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Principal:
            Service: cloudtrail.amazonaws.com
          Action: sts:AssumeRole
      Policies:
      - PolicyName: cloudtrail-policy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: !Sub ${TrailLogGroup.Arn}

  Trail:
    Type: AWS::CloudTrail::Trail
    Properties:
      IncludeGlobalServiceEvents: true
      IsLogging: true
      IsMultiRegionTrail: false
      S3BucketName: !Sub ${TrailBucket}
      CloudWatchLogsLogGroupArn: !Sub ${TrailLogGroup.Arn}
      CloudWatchLogsRoleArn: !Sub ${TrailLogGroupRole.Arn}
    DependsOn: TrailBucketPolicy

  TrailLogtoLambda:
    Type: AWS::Logs::SubscriptionFilter
    Properties:
      DestinationArn: !Sub ${LogStreamerArn}
      FilterPattern: !FindInMap
        - FilterPatternLookup
        - CloudTrail
        - Pattern
      LogGroupName: !Sub ${TrailLogGroup}

  #
  # WebServer log resources
  # [EC2LogRole, InstanceProfile, WebServerSecurityGroup, WebServerSecurityGroupIngress, WebServerLogtoLambda]
  #
  EC2LogRole:
    Type: AWS::IAM::Role  
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: LogRolePolicy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
              - logs:Create*
              - logs:PutLogEvents
              - s3:GetObject
              Resource:
              - !Sub ${WebServerLogGroup.Arn}
              - !GetAtt TrailBucket.Arn

  InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
      - !Sub ${EC2LogRole}

  WebServerHost:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install a simple PHP application
      AWS::CloudFormation::Init:
        config:
          packages:
            yum:
              httpd: []
              php: []
          files:
            /tmp/cwlogs/apacheaccess.conf:
              content: !Join
                - ''
                - - |
                    [general]
                  - |
                    state_file= /var/awslogs/agent-state
                  - |
                    [/var/log/httpd/access_log]
                  - |
                    file = /var/log/httpd/access_log
                  - 'log_group_name = '
                  - !Ref WebServerLogGroup
                  - |+

                  - |
                    log_stream_name = {instance_id}/apache.log
                  - 'datetime_format = %d/%b/%Y:%H:%M:%S'
              mode: '000400'
              owner: apache
              group: apache
            /var/www/html/index.php:
              content: !Join
                - ''
                - - |
                    <?php
                  - |
                    echo '<h1>AWS CloudFormation sample PHP application</h1>';
                  - |
                    ?>
              mode: '000644'
              owner: apache
              group: apache
            /etc/cfn/cfn-hup.conf:
              content: !Join
                - ''
                - - |
                    [main]
                  - stack=
                  - !Ref 'AWS::StackId'
                  - |+

                  - region=
                  - !Ref 'AWS::Region'
                  - |+

              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Join
                - ''
                - - |
                    [cfn-auto-reloader-hook]
                  - |
                    triggers=post.update
                  - >
                    path=Resources.WebServerHost.Metadata.AWS::CloudFormation::Init
                  - 'action=/opt/aws/bin/cfn-init -s '
                  - !Ref 'AWS::StackId'
                  - ' -r WebServerHost '
                  - ' --region     '
                  - !Ref 'AWS::Region'
                  - |+

                  - |
                    runas=root
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              sendmail:
                enabled: 'false'
                ensureRunning: 'false'
    CreationPolicy:
      ResourceSignal:
        Timeout: PT15M
    Properties:
      ImageId: !Ref LatestAMIId
      Tags:
      - Key: Name
        Value: Web Server centralized-logging-demo
      NetworkInterfaces:
      - GroupSet:
        - !Sub ${WebServerSecurityGroup}
        AssociatePublicIpAddress: true
        DeviceIndex: 0
        DeleteOnTermination: true
        SubnetId: !Sub ${PublicSubnet}
      InstanceType: !FindInMap [EC2, Instance, Type]
      IamInstanceProfile: !Sub ${InstanceProfile}
      UserData: !Base64
        'Fn::Join':
          - ''
          - - |
              #!/bin/bash -xe
            - |
              # Get the latest CloudFormation package
            - |
              yum update -y aws-cfn-bootstrap
            - |
              # Start cfn-init
            - '/opt/aws/bin/cfn-init -s '
            - !Ref 'AWS::StackId'
            - ' -r WebServerHost '
            - ' --region '
            - !Ref 'AWS::Region'
            - |2
               || error_exit 'Failed to run cfn-init'
            - >
              # Start up the cfn-hup daemon to listen for changes to the EC2
              instance metadata
            - |
              /opt/aws/bin/cfn-hup || error_exit 'Failed to start cfn-hup'
            - |
              # Get the CloudWatch Logs agent
            - >
              wget
              https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py
            - |
              # Install the CloudWatch Logs agent
            - 'python awslogs-agent-setup.py -n -r '
            - !Ref 'AWS::Region'
            - |2
               -c /tmp/cwlogs/apacheaccess.conf || error_exit 'Failed to run CloudWatch Logs agent setup'
            - |
              # pre-warm the apache logs
            - |
              curl 127.0.0.1
            - |
              curl 127.0.0.1/404
            - |
              # All done so signal success
            - '/opt/aws/bin/cfn-signal -e $? '
            - '         --stack '
            - !Ref 'AWS::StackName'
            - '         --resource WebServerHost '
            - '         --region '
            - !Ref 'AWS::Region'
            - |+

  WebServerLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 1

  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    ##
    # V56617021 - 10/08/2018 - Suppress cfn_nag
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F1000
            reason: Override the SecurityGroup, egress rule defined as well
    Properties:
      GroupDescription: Enable HTTP access via port 80
      VpcId: !Sub ${DemoVPC}

  WebServerSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Allow inbound access to the webserver from the internet
      GroupId: !Sub ${WebServerSecurityGroup}
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      CidrIp: 0.0.0.0/0
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W9
            reason: "The server is a public webserver and hence CIDR mask is open"
          - id: W2
            reason: "To demo the functionality of this solution, there is public facing webserver with which the SecurityGroup will be associated. This webserver instance will be used only as a mechanism to generate logs. Hence no ELB has been added to the infrastructure"

  WebServerSecurityGroupEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      Description: Allow outbound access to the internet for all requests coming in
      CidrIp: 0.0.0.0/0
      FromPort: 0
      GroupId: !Sub ${WebServerSecurityGroup}
      IpProtocol: tcp
      ToPort: 65535
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W5
            reason: "The webserver is a public access server and hence CIDR range is open"
          - id: W29
            reason: "The TCP/IP protocol requires the ephemeral port range for communication over the internet for a public server"

  WebServerLogtoLambda:
    Type: AWS::Logs::SubscriptionFilter
    Properties:
      DestinationArn: !Sub ${LogStreamerArn}
      FilterPattern: !FindInMap [FilterPatternLookup, Common, Pattern]
      LogGroupName: !Sub ${WebServerLogGroup}

Outputs:
  PublicIP:
    Description: Public IP of sample web server
    Value: !Sub http://${WebServerHost.PublicIp}
