Repository: aws-cloudformation/cfn-lint

Issue:
CloudWatchLogGroups are must not duplicated in the Lambda's Events section
*cfn-lint version: cfn-lint 0.9.0*

*Description of issue.*

![image](https://user-images.githubusercontent.com/20736455/55238033-e1d59400-5276-11e9-9619-13c308678d86.png)

In this configuration, Lambda for log analysis (LogSubscriptionFunction in this figure) subscribes and processes multiple Lambda logs output to CloudWatchLogs.
CloudFormation templates can be described as follows.

```.yaml
  LogSubscriptionFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: subscription_function/build/
      Handler: app.lambda_handler
      Runtime: python3.6
      Events:
        FunctionALogGroup:
          Type: CloudWatchLogs
          Properties:
            LogGroupName: !Ref FunctionALogGroup
            FilterPattern: ""
        FunctionBLogGroup:
          Type: CloudWatchLogs
          Properties:
            LogGroupName: !Ref FunctionALogGroup # <--- log group is duplicated !!
            FilterPattern: ""
```

If the log group is duplicated in the Events section, the following error occurs when creating a stack:

> Resource limit exceeded. (Service: AWSLogs; Status Code: 400; Error Code: LimitExceededException; Request ID: 565efb75-522a-11e9-a6c3-61534eead0f1)


I think that it is necessary to have an implementation that causes an error by Linter when LogGroup is described in duplicate.



Discussion:
@chuckmeyer and @rjlohan is this the same idea of the route table association to subnet check we have?

Resolving patch:
diff --git a/src/cfnlint/rules/resources/lmbd/EventsLogGroupName.py b/src/cfnlint/rules/resources/lmbd/EventsLogGroupName.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/lmbd/EventsLogGroupName.py
@@ -0,0 +1,68 @@
+"""
+  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+from cfnlint import CloudFormationLintRule
+from cfnlint import RuleMatch
+
+
+class EventsLogGroupName(CloudFormationLintRule):
+    """Check if the settings of multiple subscriptions are included for one LogGroup"""
+    id = 'E2529'
+    shortdesc = 'Check for duplicate Lambda events'
+    description = 'Check if there are any duplicate log groups in the Lambda event trigger element.'
+    source_url = 'https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#user-content-cloudwatchlogs'
+    tags = ['resources', 'lambda']
+
+    def check_events_subscription_duplicated(self, cfn):
+        """Check if Lambda Events Subscription is duplicated"""
+        matches = []
+        message = 'You must specify the AWS::Serverless::Function event correctly. ' \
+                  'LogGroups are duplicated. '
+
+        log_group_name_list = self.__get_log_group_name_list(cfn)
+
+        if self.__is_duplicated(log_group_name_list):
+            matches.append(
+                RuleMatch(
+                    'path', message.format()
+                )
+            )
+
+        return matches
+
+    def __is_duplicated(self, duplicate_list):
+        unique_list = self.__remove(duplicate_list)
+        return len(unique_list) != len(duplicate_list)
+
+    def __remove(self, duplicate):
+        final_list = []
+        for ele in duplicate:
+            if ele not in final_list:
+                final_list.append(ele)
+        return final_list
+
+    def __get_log_group_name_list(self, cfn):
+        log_group_name_list = []
+        for value in cfn.get_resources('AWS::Logs::SubscriptionFilter').items():
+            prop = value[1].get('Properties')
+            log_group_name_list.append(prop.get('LogGroupName'))
+        return log_group_name_list
+
+    def match(self, cfn):
+        """Check if Lambda Events Subscription is duplicated"""
+        matches = []
+        matches.extend(
+            self.check_events_subscription_duplicated(cfn)
+        )
+        return matches


Test patch:
diff --git a/test/fixtures/templates/bad/some_logs_stream_lambda.yaml b/test/fixtures/templates/bad/some_logs_stream_lambda.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/some_logs_stream_lambda.yaml
@@ -0,0 +1,77 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: >
+  subscription-filter-failed-template
+
+  Sample SAM Template for subscription-filter-failed-template
+
+# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
+Globals:
+  Function:
+      Timeout: 30
+
+
+Resources:
+
+  FunctionA:
+    Type: AWS::Serverless::Function
+    Properties:
+      CodeUri: hello_world/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+  FunctionALogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${FunctionA}
+      RetentionInDays: 3
+
+  FunctionB:
+    Type: AWS::Serverless::Function
+    Properties:
+      CodeUri: hello_world/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+  FunctionBLogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${FunctionB}
+      RetentionInDays: 3
+  FunctionC:
+    Type: AWS::Serverless::Function
+    Properties:
+      CodeUri: hello_world/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+  FunctionCLogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${FunctionC}
+      RetentionInDays: 3
+
+  LogSubscriptionFunction:
+    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
+    Properties:
+      CodeUri: subscription_function/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+      Events:
+        FunctionALogGroup:
+          Type: CloudWatchLogs
+          Properties:
+            LogGroupName: !Ref FunctionALogGroup
+            FilterPattern: ""
+        FunctionBLogGroup:
+          Type: CloudWatchLogs
+          Properties:
+            LogGroupName: !Ref FunctionBLogGroup
+            FilterPattern: ""
+        FunctionCLogGroup:
+          Type: CloudWatchLogs
+          Properties:
+            LogGroupName: !Ref FunctionALogGroup
+            FilterPattern: ""
+  LogSubscriptionFunctionLogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${LogSubscriptionFunction}
+      RetentionInDays: 3
diff --git a/test/fixtures/templates/good/some_logs_stream_lambda.yaml b/test/fixtures/templates/good/some_logs_stream_lambda.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/some_logs_stream_lambda.yaml
@@ -0,0 +1,77 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: >
+  subscription-filter-failed-template
+
+  Sample SAM Template for subscription-filter-failed-template
+
+# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
+Globals:
+  Function:
+      Timeout: 30
+
+
+Resources:
+
+  FunctionA:
+    Type: AWS::Serverless::Function
+    Properties:
+      CodeUri: hello_world/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+  FunctionALogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${FunctionA}
+      RetentionInDays: 3
+
+  FunctionB:
+    Type: AWS::Serverless::Function
+    Properties:
+      CodeUri: hello_world/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+  FunctionBLogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${FunctionB}
+      RetentionInDays: 3
+  FunctionC:
+    Type: AWS::Serverless::Function
+    Properties:
+      CodeUri: hello_world/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+  FunctionCLogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${FunctionC}
+      RetentionInDays: 3
+
+  LogSubscriptionFunction:
+    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
+    Properties:
+      CodeUri: subscription_function/build/
+      Handler: app.lambda_handler
+      Runtime: python3.6
+      Events:
+        FunctionALogGroup:
+          Type: CloudWatchLogs
+          Properties:
+            LogGroupName: !Ref FunctionALogGroup
+            FilterPattern: ""
+        FunctionBLogGroup:
+          Type: CloudWatchLogs
+          Properties:
+            LogGroupName: !Ref FunctionBLogGroup
+            FilterPattern: ""
+        FunctionCLogGroup:
+          Type: CloudWatchLogs
+          Properties:
+            LogGroupName: !Ref FunctionCLogGroup
+            FilterPattern: ""
+  LogSubscriptionFunctionLogGroup:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Sub /aws/lambda/${LogSubscriptionFunction}
+      RetentionInDays: 3
diff --git a/test/rules/resources/lmbd/test_events_log_group_name.py b/test/rules/resources/lmbd/test_events_log_group_name.py
new file mode 100644
--- /dev/null
+++ b/test/rules/resources/lmbd/test_events_log_group_name.py
@@ -0,0 +1,37 @@
+"""
+  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+from cfnlint.rules.resources.lmbd.EventsLogGroupName import EventsLogGroupName
+from ... import BaseRuleTestCase
+
+
+class TestEventsLogGroupName(BaseRuleTestCase):
+    """Test Lambda Trigger Events CloudWatchLogs Property Configuration"""
+    def setUp(self):
+        """Setup"""
+        super(TestEventsLogGroupName, self).setUp()
+        self.collection.register(EventsLogGroupName())
+        self.success_templates = [
+            'test/fixtures/templates/good/some_logs_stream_lambda.yaml'
+        ]
+
+    def test_file_positive(self):
+        """Test Positive"""
+        self.helper_file_positive()
+
+    def test_file_negative(self):
+        """Test failure"""
+        self.helper_file_negative('test/fixtures/templates/bad/some_logs_stream_lambda.yaml', 1)