AWSTemplateFormatVersion: 2010-09-09
Description: This template creates resources
# Sample code, software libraries, command line tools, proofs of concept, templates, or other related technology are provided as AWS Content or Third-Party Content under the AWS Customer Agreement, or the relevant written agreement between you and AWS (whichever applies). You should not use this AWS Content or Third-Party Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the AWS Content or Third-Party Content, such as sample code, as appropriate for production grade use based on your specific quality control practices and standards. Deploying AWS Content or Third-Party Content may incur AWS charges for creating or using AWS chargeable resources, such as running Amazon EC2 instances or using Amazon S3 storage. Refer to associated README for additional guidance and considerations.

Resources:
  # Define the Lambda function that detects duplicate AWS Config rules
  DuplicateRuleDetectionLambda:
    Type: "AWS::Lambda::Function"
    Properties:
      Role: !GetAtt DetectionLambdaRole.Arn
      Code:
        ZipFile: |
          import json
          import boto3
          from datetime import datetime
          import os
          from concurrent.futures import ThreadPoolExecutor, as_completed

          configClient = boto3.client('config')
          s3Client = boto3.client('s3')

          def lambda_handler(event, context):
              all_config_rules = get_all_config_rules()
              repeated_rules = find_equivalent_active_rules(all_config_rules)
              upload_to_s3(repeated_rules)
              return {'statusCode': 200, 'body': repeated_rules}

          def get_all_config_rules():
              all_config_rules = []
              next_token = None
              while True:
                  if next_token:
                      response = configClient.describe_config_rules(NextToken=next_token)
                  else:
                      response = configClient.describe_config_rules()
                  all_config_rules.extend(response["ConfigRules"])
                  next_token = response.get("NextToken")
                  if not next_token:
                      break
              return all_config_rules

          def find_equivalent_active_rules(config_rules):
              checked_rules = set()
              equivalent_rules = {}
              with ThreadPoolExecutor(max_workers=5) as executor:
                  futures = []
                  for config_rule in config_rules:
                      arn = config_rule["ConfigRuleArn"]
                      if arn not in checked_rules and config_rule["ConfigRuleState"] == "ACTIVE":
                          futures.append(executor.submit(compare_and_group_rules, config_rule, config_rules, checked_rules, equivalent_rules))
                  for future in as_completed(futures):
                      future.result()
              return {key: rules for key, rules in equivalent_rules.items() if len(rules) > 1}

          def compare_and_group_rules(config_rule, config_rules, checked_rules, equivalent_rules):
            arn = config_rule["ConfigRuleArn"]
            checked_rules.add(arn)
            equivalent_rules[arn] = [config_rule]
            
            for compared_rule in config_rules:
                compared_arn = compared_rule["ConfigRuleArn"]
                
                if (
                    compared_arn not in checked_rules and
                    config_rule["Source"] == compared_rule["Source"] and
                    compare_scope(config_rule, compared_rule) and
                    config_rule.get("InputParameters") == compared_rule.get("InputParameters") and
                    compared_rule["ConfigRuleState"] == "ACTIVE"
                ):
                    checked_rules.add(compared_arn)
                    equivalent_rules[arn].append(compared_rule)


          def upload_to_s3(data):
              timestamp = datetime.now().strftime("Date-%d-%m-%Y Time-%H-%M-%S.json")
              bucket_name = os.environ.get('S3BucketName')
              s3Client.put_object(Body=json.dumps(list(data.values()), indent=4), Bucket=bucket_name, Key=timestamp)
              return timestamp

          def compare_scope(rule1, rule2):
              match rule1.get("Scope"), rule2.get("Scope"):
                  case None, None:
                      return True
                  case {"ComplianceResourceTypes": []}, None | None, {"ComplianceResourceTypes": []}:
                      return True
                  case {"ComplianceResourceTypes": types1}, {"ComplianceResourceTypes": types2} if types1 == types2:
                      return True
                  case _:
                      return False

      ReservedConcurrentExecutions: 5
      Runtime: python3.12
      Handler: index.lambda_handler
      Architectures:
        - arm64
      Timeout: 300
      Environment:
        Variables:
          S3BucketName: !Ref DetectionLambdaResultsBucket

  # Define the IAM policy for the Lambda function to describe Config rules and upload to S3
  DetectionLambdaPolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: DuplicateConfigRuleDetectionLambdaPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ListConfigRules
            Effect: Allow
            Action:
              - "config:DescribeConfigRules"
            Resource: !Sub "arn:aws:config:${AWS::Region}:${AWS::AccountId}:config-rule/*"
          - Sid: PutObjectsToDetectionLambdaBucket
            Effect: Allow
            Action:
              - "s3:PutObject"
              - "s3:PutObjectAcl"
            Resource: !Sub "${DetectionLambdaResultsBucket.Arn}/*"
      Roles:
        - !Ref DetectionLambdaRole

  # Define the IAM role for the Lambda function
  DetectionLambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"

  # Define the S3 bucket for storing the results
  DetectionLambdaResultsBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  # Define the S3 Bucket Policy to grant read and write permissions
  DetectionLambdaResultsBucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref DetectionLambdaResultsBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          # Grant list and read access to any user in the account
          - Sid: ListAndReadAccessForAccountUsers
            Effect: Allow
            Action: 
              - "s3:GetObject"
              - "s3:ListBucket"
            Resource:
              - !Sub "${DetectionLambdaResultsBucket.Arn}/*"  # For object-level actions like GetObject
              - !Sub "${DetectionLambdaResultsBucket.Arn}"     # For bucket-level actions like ListBucket
            Principal: "*"
            Condition:
              StringEquals:
                "aws:SourceAccount": !Sub "${AWS::AccountId}"

          # Grant write access and object ACL management only to the Lambda function
          - Sid: WriteAccessForLambda
            Effect: Allow
            Action: 
              - "s3:PutObject"
              - "s3:PutObjectAcl"
            Resource: !Sub "${DetectionLambdaResultsBucket.Arn}/*"
            Principal:
              AWS: !GetAtt DetectionLambdaRole.Arn

  # EventBridge rule to trigger the Lambda function periodically
  DetectionLambdaEventBridgeRule:
    Type: "AWS::Events::Rule"
    Properties:
      Description: >-
        This rule is used to trigger the DuplicateRuleDetectionLambda that
        detects potential duplicated AWS Config Rules that you have deployed in
        your AWS Account.
      ScheduleExpression: cron(0 0 1 * ? *)
      Targets:
        - Arn: !GetAtt DuplicateRuleDetectionLambda.Arn
          Id: TargetDuplicateConfigRuleDetectionLambda
