---
AWSTemplateFormatVersion: '2010-09-09'
Description: Demonstrates how to use GuardDuty Findings to automate WAFv2 ACL and VPC NACL entries.
  The template installs a Lambda function that updates an AWS WAFv2 IP Set and VPC NACL.

Parameters:
  Retention:
    Description: How long to retain IP addresses in the blocklist (in minutes). Default is 12 hours, minimum is 5 minutes and maximum one week (10080 minutes)
    Type: Number
    Default: 720
    MinValue: 5
    MaxValue: 10080
    ConstraintDescription: Minimum of 5 minutes and maximum of 10080 (one week).
  AdminEmail:
    Description: Email address to receive notifications. Must be a valid email address.
    Type: String
    AllowedPattern: ^(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$
  EnableCloudFront:
    Type: String
    Default: True
    AllowedValues:
      - True
      - False
    Description: CloudFront resources (WebACL and IP sets) will be created automatically.
  ArtifactsBucket:
    Description: S3 bucket with artifact files (Lambda functions, templates, html files, etc.).
    Type: String
    AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$
    ConstraintDescription: ArtifactsBucket S3 bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-).
      It cannot start or end with a hyphen (-).
  ArtifactsPrefix:
    Description: Path in the S3 folder, e.g. "folder_name/" containing artifact files. Leave empty if artifacts are in root of S3 bucket.
    Type: String
    Default: ""
    AllowedPattern: ^[0-9a-zA-Z-/|]*$
    ConstraintDescription: ArtifactsPrefix key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-),
      and forward slash (/). Leave empty if artifacts are in root of S3 bucket.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: GD2ACL Configuration
      Parameters:
      - AdminEmail
      - Retention
      - EnableCloudFront
    - Label:
        default: Artifact Configuration
      Parameters:
      - ArtifactsBucket
      - ArtifactsPrefix

    ParameterLabels:
      AdminEmail:
        default: Notification email (REQUIRED)
      Retention:
        default: Retention time in minutes
      EnableCloudFront:
        default: Create CloudFront Web ACL and IP set?
      ArtifactsBucket:
        default: S3 bucket for artifacts
      ArtifactsPrefix:
        default: S3 path to artifacts

Conditions:
# Create CloudFront resources?
  CloudFrontEnable: !Equals [True, !Ref EnableCloudFront ]

Resources:

  GuardDutytoACLLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: "GuardDuty to ACL Function"
      Architectures:
        - arm64
      Handler : "guardduty_to_acl_lambda.lambda_handler"
      MemorySize: 1024
      Timeout: 300
      Role: !GetAtt GuardDutytoACLRole.Arn
      Runtime : "python3.11"
      Environment:
        Variables:
          ACLMETATABLE: !Ref GuardDutytoACLDDBTable
          REGIONAL_IP_SET: !Ref RegionalBlocklistIPSetV4
          CLOUDFRONT_IP_SET: !If [CloudFrontEnable, !Ref CloudFrontBlocklistIPSetV4, '']
          SNSTOPIC: !Ref GuardDutytoACLSNSTopic
          CLOUDFRONT_ENABLE: !Ref EnableCloudFront
      Code:
        S3Bucket: !Sub ${ArtifactsBucket}
        S3Key: !Sub ${ArtifactsPrefix}guardduty_to_acl_lambda_wafv2.zip

  GuardDutytoACLRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              "sts:AssumeRole"
      Path: "/"

  GuardDutytoACLPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName:
        Fn::Join:
          - '-'
          - [ !Ref "AWS::Region", 'guardduty-to-nacl-wafipset' ]
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Action:
            - wafv2:GetIPSet
            - wafv2:UpdateIPSet
            Resource:
              Fn::If:
                - CloudFrontEnable
                - - !GetAtt CloudFrontBlocklistIPSetV4.Arn
                  - !GetAtt RegionalBlocklistIPSetV4.Arn
                - - !GetAtt RegionalBlocklistIPSetV4.Arn
          -
            Effect: "Allow"
            Action:
              - "ec2:Describe*"
              - "ec2:*NetworkAcl*"
            Resource: "*"
          -
            Effect: "Allow"
            Action:
              - "logs:CreateLogGroup"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
            Resource: "arn:aws:logs:*:*:*"
          -
            Effect: Allow
            Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:DeleteItem
            Resource: !GetAtt GuardDutytoACLDDBTable.Arn
          -
            Effect: Allow
            Action:
            - sns:Publish
            Resource: !Ref GuardDutytoACLSNSTopic
      Roles:
        -
          Ref: "GuardDutytoACLRole"

  # GuardDuty CloudWatch Event - For GuardDuty Finding:
  GuardDutytoACLEvent:
    Type: "AWS::Events::Rule"
    Properties:
      Description: "GuardDuty Malicious Host Events"
      EventPattern:
        source:
        - aws.guardduty
        detail:
          type:
            - prefix: "UnauthorizedAccess:EC2"
            - prefix: "Recon:EC2"
            - prefix: "Trojan:EC2"
            - prefix: "Backdoor:EC2"
            - prefix: "Impact:EC2"
            - prefix: "CryptoCurrency:EC2"
            - prefix: "Behavior:EC2"

      State: "ENABLED"
      Targets:
        -
          Arn: !GetAtt GuardDutytoACLLambda.Arn
          Id: "GuardDutyEvent-Lambda-Trigger"

  GuardDutytoACLInvokePermissions:
    Type: "AWS::Lambda::Permission"
    Properties:
      FunctionName: !Ref "GuardDutytoACLLambda"
      Action: "lambda:InvokeFunction"
      SourceArn: !GetAtt GuardDutytoACLEvent.Arn
      Principal: "events.amazonaws.com"

  GuardDutytoACLDDBTable:
    Type: "AWS::DynamoDB::Table"
    Properties:
      AttributeDefinitions:
        -
          AttributeName: "NetACLId"
          AttributeType: "S"
        -
          AttributeName: "CreatedAt"
          AttributeType: "N"
      KeySchema:
        -
          AttributeName: "NetACLId"
          KeyType: "HASH"
        -
          AttributeName: "CreatedAt"
          KeyType: "RANGE"
      ProvisionedThroughput:
        ReadCapacityUnits: "5"
        WriteCapacityUnits: "5"

  CloudFrontBlocklistIPSetV4:
    Condition: CloudFrontEnable
    Type: 'AWS::WAFv2::IPSet'
    Properties:
      Description: GD2ACLIPSetV4
      Scope: CLOUDFRONT
      IPAddressVersion: IPV4
      Addresses:
        - 127.0.0.0/8
        
  CloudFrontBlocklistIPSetV6:
    Condition: CloudFrontEnable
    Type: 'AWS::WAFv2::IPSet'
    Properties:
      Description: GD2ACLIPSetV6
      Scope: CLOUDFRONT
      IPAddressVersion: IPV6
      Addresses:
        - ::1/128

  CloudFrontBlocklistWebACL:
    Condition: CloudFrontEnable
    Type: AWS::WAFv2::WebACL
    Properties:
      Scope: CLOUDFRONT
      DefaultAction:
        Allow: {}
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: GD2ACLCloudFrontBlocklistWebACL
      Rules:
        - Name: CloudFrontBlocklistIPSetRule
          Priority: 1
          Action:
            Block: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: CloudFrontBlockIPMetric
          Statement:
            OrStatement:
              Statements:
              - IPSetReferenceStatement: 
                  Arn: !If [CloudFrontEnable, !GetAtt CloudFrontBlocklistIPSetV4.Arn, '']
              - IPSetReferenceStatement:
                  Arn: !GetAtt CloudFrontBlocklistIPSetV6.Arn

  RegionalBlocklistIPSetV4:
    Type: 'AWS::WAFv2::IPSet'
    Properties:
      Description: GD2ACLIPSetV4
      Scope: REGIONAL
      IPAddressVersion: IPV4
      Addresses:
        - 127.0.0.0/8
        
  RegionalBlocklistIPSetV6:
    Type: 'AWS::WAFv2::IPSet'
    Properties:
      Description: GD2ACLIPSetV6
      Scope: REGIONAL
      IPAddressVersion: IPV6
      Addresses:
        - ::1/128

  RegionalBlocklistWebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Scope: REGIONAL
      DefaultAction:
        Allow: {}
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: GD2ACLRegionalBlocklistWebACL
      Rules:
        - Name: RegionalBlocklistIPSetRule
          Priority: 1
          Action:
            Block: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: RegionalBlockIPMetric
          Statement:
            OrStatement:
              Statements:
              - IPSetReferenceStatement: 
                  Arn: !GetAtt RegionalBlocklistIPSetV4.Arn
              - IPSetReferenceStatement:
                  Arn: !GetAtt RegionalBlocklistIPSetV6.Arn

  PruneOldEntriesLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: "Prune old entries in WAF ACL and NACLs"
      Architectures:
        - arm64
      Handler : "prune_old_entries.lambda_handler"
      MemorySize: 1024
      Timeout: 300
      Role: !GetAtt PruneOldEntriesRole.Arn
      Runtime : "python3.11"
      Environment:
        Variables:
          ACLMETATABLE: !Ref GuardDutytoACLDDBTable
          REGIONAL_IP_SET: !Ref RegionalBlocklistIPSetV4
          CLOUDFRONT_IP_SET: !If [CloudFrontEnable, !Ref CloudFrontBlocklistIPSetV4, '']
          RETENTION: !Ref Retention
          CLOUDFRONT_ENABLE: !Ref EnableCloudFront
      Code:
        S3Bucket: !Sub ${ArtifactsBucket}
        S3Key: !Sub ${ArtifactsPrefix}prune_old_entries_wafv2.zip

  PruneOldEntriesRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              "sts:AssumeRole"
      Path: "/"

  PruneOldEntriesPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName:
        Fn::Join:
          - '-'
          - [ !Ref "AWS::Region", 'prune-old-entries' ]
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Action:
            - wafv2:GetIPSet
            - wafv2:UpdateIPSet
            Resource:
              Fn::If:
                - CloudFrontEnable
                - - !GetAtt CloudFrontBlocklistIPSetV4.Arn
                  - !GetAtt RegionalBlocklistIPSetV4.Arn
                - - !GetAtt RegionalBlocklistIPSetV4.Arn
          -
            Effect: "Allow"
            Action:
              - "ec2:Describe*"
              - "ec2:*NetworkAcl*"
            Resource: "*"
          -
            Effect: "Allow"
            Action:
              - "logs:CreateLogGroup"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
            Resource: "arn:aws:logs:*:*:*"
          -
            Effect: Allow
            Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:DeleteItem
            Resource: !GetAtt GuardDutytoACLDDBTable.Arn
      Roles:
        -
          Ref: "PruneOldEntriesRole"

  PruneOldEntriesSchedule:
    Type: "AWS::Events::Rule"
    Properties:
      Description: "ScheduledPruningRule"
      ScheduleExpression: "rate(5 minutes)"
      State: "ENABLED"
      Targets:
        -
          Arn: !GetAtt PruneOldEntriesLambda.Arn
          Id: "TargetFunctionV1"

  PruneOldEntriesPermissionToInvoke:
    DependsOn:
      - GuardDutytoACLLambda
    Type: "AWS::Lambda::Permission"
    Properties:
      FunctionName: !Ref PruneOldEntriesLambda
      Action: "lambda:InvokeFunction"
      Principal: "events.amazonaws.com"
      SourceArn: !GetAtt PruneOldEntriesSchedule.Arn

  GuardDutytoACLSNSTopic:
    Type: "AWS::SNS::Topic"
    Properties:
      Subscription:
        -
          Endpoint: !Ref AdminEmail
          Protocol: "email"

Outputs:
  GuardDutytoACLLambda:
    Description: GD2ACL Primary Lambda Function.
    Value: !Sub https://console.aws.amazon.com/lambda/home?region=${AWS::Region}#/functions/${GuardDutytoACLLambda}
  PruneOldEntriesLambda:
    Description: GD2ACL Entry Pruning Lambda Function.
    Value: !Sub https://console.aws.amazon.com/lambda/home?region=${AWS::Region}#/functions/${PruneOldEntriesLambda}
  ACLMetaTable:
    Description: GD2ACL DynamoDB State Table
    Value: !Ref GuardDutytoACLDDBTable
  RegionalIPSetId:
    Description: Regional IP Set
    Value: !Ref RegionalBlocklistIPSetV4
  CloudFrontIPSetId:
    Condition: CloudFrontEnable
    Description: CloudFront IP Set
    Value: !Ref CloudFrontBlocklistIPSetV4
  RegionalWebACL:
    Description: Regional Web ACL
    Value: !Ref RegionalBlocklistWebACL
  CloudFrontWebACL:
    Condition: CloudFrontEnable
    Description: CloudFront Web ACL
    Value: !Ref CloudFrontBlocklistWebACL
  Retention:
    Description: ACL Entry Time to Live in Minutes
    Value: !Ref Retention
  Region:
    Description: Region of the stack.
    Value:
      Ref: AWS::Region