Repository: aws-cloudformation/cfn-lint

Issue:
I3013 - Incorrectly flagged for aurora-<engine> database instances
**cfn-lint version**: 0.61.3

cfn-lint is falsely flagging the missing `BackupRetentionPeriod` property and it's subsequent value from `AWS::RDS:DBInstance` for an Aurora based database instance.

This property and a set value **is not required** if it is part of an Aurora cluster. Assumption is the `Engine` property value would need to be checked for the presence of `aurora-<engine>` to not flag this linting issue.

I suspect if the value is set via a CloudFormation function cfn-lint would still incorrectly flag this.

**AWS Documentation reference** - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-backupretentionperiod

> Amazon Aurora
> Not applicable. The retention period for automated backups is managed by the DB cluster.

**CloudFormation example**

```yaml
AuroraInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      AllowMajorVersionUpgrade: false
      AutoMinorVersionUpgrade: !Ref AutoMinorVersionUpgrade
      DBClusterIdentifier: !Ref AuroraCluster
      DBInstanceClass: !Ref InstanceClass
      DBInstanceIdentifier: "MyAuroraInstance"
      DBParameterGroupName: !Ref ParamGroup
      DBSubnetGroupName: !Ref SubnetGroup
      DeleteAutomatedBackups: !Ref DeleteAutomatedBackups
      EnablePerformanceInsights: !Ref EnablePerformanceInsights
      Engine: aurora-postgresql
      EngineVersion: !Ref EngineVersion
      PerformanceInsightsKMSKeyId: !Ref KmsKey
      PerformanceInsightsRetentionPeriod: 7
      PubliclyAccessible: false
```



Discussion:
https://github.com/aws-cloudformation/cfn-lint/issues/2311

---

(nice to see more informational rule feedback coming in, assuming https://github.com/aws-cloudformation/cfn-lint-visual-studio-code/pull/223 is surfacing them more based on that being released this week)

Resolving patch:
diff --git a/src/cfnlint/rules/resources/RetentionPeriodOnResourceTypesWithAutoExpiringContent.py b/src/cfnlint/rules/resources/RetentionPeriodOnResourceTypesWithAutoExpiringContent.py
--- a/src/cfnlint/rules/resources/RetentionPeriodOnResourceTypesWithAutoExpiringContent.py
+++ b/src/cfnlint/rules/resources/RetentionPeriodOnResourceTypesWithAutoExpiringContent.py
@@ -2,6 +2,7 @@
 Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 SPDX-License-Identifier: MIT-0
 """
+import re
 from cfnlint.rules import CloudFormationLintRule
 from cfnlint.rules import RuleMatch
 
@@ -17,7 +18,6 @@ class RetentionPeriodOnResourceTypesWithAutoExpiringContent(CloudFormationLintRu
     tags = ['resources', 'retentionperiod']
 
     def _check_ref(self, value, parameters, resources, path):  # pylint: disable=W0613
-        print('called')
         print(value)
 
     def match(self, cfn):
@@ -62,7 +62,9 @@ def match(self, cfn):
             'AWS::RDS::DBInstance': [
                 {
                     'Attribute': 'BackupRetentionPeriod',
-                    'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-backupretentionperiod'
+                    'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-backupretentionperiod',
+                    'CheckAttribute': 'Engine',
+                    'CheckAttributeRegex': re.compile('^((?!aurora).)*$'),
                 }
             ],
             'AWS::RDS::DBCluster': [
@@ -81,23 +83,29 @@ def match(self, cfn):
                     for property_set, path in property_sets:
                         error_path = ['Resources', r_name] + path
                         if not property_set:
-                            message = 'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : %s' % '/'.join(
-                                str(x) for x in error_path)
+                            message = f'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : {"/".join(str(x) for x in error_path)}'
                             matches.append(RuleMatch(error_path, message))
                         else:
                             value = property_set.get(attr_def.get('Attribute'))
                             if not value:
-                                message = 'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : %s' % '/'.join(
-                                    str(x) for x in error_path)
-                                matches.append(RuleMatch(error_path, message))
+                                message = f'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : {"/".join(str(x) for x in error_path)}'
+                                if attr_def.get('CheckAttribute'):
+                                    if self._validate_property(property_set.get(attr_def.get('CheckAttribute')), attr_def.get('CheckAttributeRegex')):
+                                        matches.append(RuleMatch(error_path, message))
+                                else:
+                                    matches.append(RuleMatch(error_path, message))
                             if isinstance(value, dict):
                                 # pylint: disable=protected-access
                                 refs = cfn._search_deep_keys(
                                     'Ref', value, error_path + [attr_def.get('Attribute')])
                                 for ref in refs:
                                     if ref[-1] == 'AWS::NoValue':
-                                        message = 'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : %s' % '/'.join(
-                                            str(x) for x in ref[0:-1])
+                                        message = f'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : {"/".join(str(x) for x in ref[0:-1])}'
                                         matches.append(RuleMatch(ref[0:-1], message))
 
         return matches
+
+    def _validate_property(self, value, regex) -> bool:
+        if regex.match(value):
+            return True
+        return False


Test patch:
diff --git a/test/fixtures/templates/bad/resources/rds/retention_period.yaml b/test/fixtures/templates/bad/resources/rds/retention_period.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/rds/retention_period.yaml
@@ -0,0 +1,21 @@
+Resources:
+  AuroraInstance:
+    Type: AWS::RDS::DBInstance
+    DeletionPolicy: Retain
+    UpdateReplacePolicy: Retain
+    Properties:
+      AllowMajorVersionUpgrade: false
+      AutoMinorVersionUpgrade: !Ref AutoMinorVersionUpgrade
+      DBClusterIdentifier: !Ref AuroraCluster
+      DBInstanceClass: !Ref InstanceClass
+      DBInstanceIdentifier: "MyAuroraInstance"
+      DBParameterGroupName: !Ref ParamGroup
+      DBSubnetGroupName: !Ref SubnetGroup
+      DeleteAutomatedBackups: !Ref DeleteAutomatedBackups
+      EnablePerformanceInsights: !Ref EnablePerformanceInsights
+      Engine: sqlserver-ee
+      EngineVersion: !Ref EngineVersion
+      PerformanceInsightsKMSKeyId: !Ref KmsKey
+      PerformanceInsightsRetentionPeriod: 7
+      PubliclyAccessible: false
+      
\ No newline at end of file
diff --git a/test/fixtures/templates/good/resources/rds/retention_period.yaml b/test/fixtures/templates/good/resources/rds/retention_period.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/rds/retention_period.yaml
@@ -0,0 +1,21 @@
+Resources:
+  AuroraInstance:
+    Type: AWS::RDS::DBInstance
+    DeletionPolicy: Retain
+    UpdateReplacePolicy: Retain
+    Properties:
+      AllowMajorVersionUpgrade: false
+      AutoMinorVersionUpgrade: !Ref AutoMinorVersionUpgrade
+      DBClusterIdentifier: !Ref AuroraCluster
+      DBInstanceClass: !Ref InstanceClass
+      DBInstanceIdentifier: "MyAuroraInstance"
+      DBParameterGroupName: !Ref ParamGroup
+      DBSubnetGroupName: !Ref SubnetGroup
+      DeleteAutomatedBackups: !Ref DeleteAutomatedBackups
+      EnablePerformanceInsights: !Ref EnablePerformanceInsights
+      Engine: aurora-postgresql
+      EngineVersion: !Ref EngineVersion
+      PerformanceInsightsKMSKeyId: !Ref KmsKey
+      PerformanceInsightsRetentionPeriod: 7
+      PubliclyAccessible: false
+      
\ No newline at end of file
diff --git a/test/unit/rules/resources/test_retentionperiod.py b/test/unit/rules/resources/test_retentionperiod.py
--- a/test/unit/rules/resources/test_retentionperiod.py
+++ b/test/unit/rules/resources/test_retentionperiod.py
@@ -14,7 +14,8 @@ def setUp(self):
         super(TestRetentionPeriodOnResourceTypesWithAutoExpiringContent, self).setUp()
         self.collection.register(RetentionPeriodOnResourceTypesWithAutoExpiringContent())
         self.success_templates = [
-            'test/fixtures/templates/good/resources/sqs/retention_period.yaml'
+            'test/fixtures/templates/good/resources/sqs/retention_period.yaml',
+            'test/fixtures/templates/good/resources/rds/retention_period.yaml',
         ]
 
     def test_file_positive(self):
@@ -25,3 +26,8 @@ def test_file_negative_alias(self):
         """Test failure"""
         self.helper_file_negative(
             'test/fixtures/templates/bad/resources/sqs/retention_period.yaml', 3)
+
+    def test_file_negative_rds(self):
+        """Test failure"""
+        self.helper_file_negative(
+            'test/fixtures/templates/bad/resources/rds/retention_period.yaml', 1)