Repository: salesforce/policy_sentry (python)

Issue:
Write-policy issues when multiple ARN types listed by RDS actions table
Template file:

```yml
mode: crud
read:
- 'arn:aws:rds:us-east-1:123456789012:db:mydatabase'
```

Output:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "RdsReadDb",
            "Effect": "Allow",
            "Action": [
                "rds:DownloadDBLogFilePortion",
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        },
        {
            "Sid": "RdsReadEs",
            "Effect": "Allow",
            "Action": [
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        },
        {
            "Sid": "RdsReadOg",
            "Effect": "Allow",
            "Action": [
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        },
        {
            "Sid": "RdsReadPg",
            "Effect": "Allow",
            "Action": [
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        },
        {
            "Sid": "RdsReadRi",
            "Effect": "Allow",
            "Action": [
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        },
        {
            "Sid": "RdsReadSecgrp",
            "Effect": "Allow",
            "Action": [
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        },
        {
            "Sid": "RdsReadSnapshot",
            "Effect": "Allow",
            "Action": [
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        },
        {
            "Sid": "RdsReadSubgrp",
            "Effect": "Allow",
            "Action": [
                "rds:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
            ]
        }
    ]
}
```

I'll investigate and make a fix for this ASAP.

Pull request:
Fixes #142 - ARN matching mechanism and also adds better logging for the write-policy command

Patch:
diff --git a/.pylintrc b/.pylintrc
index 4810d9cb..3ed3013b 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -156,7 +156,8 @@ disable=print-statement,
         expression-not-assigned,
         invalid-name,
         logging-too-many-args,
-        bad-continuation
+        bad-continuation,
+        logging-format-interpolation
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f6a2ebc..d6e9f4e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 ## Future release (Unreleased)
 * Add conditions support
 
+## 0.7.1.3 (Unreleased)
+* Better logging for the write-policy command
+* Fixes #142 - the issue with validating matching resource ARN types
+
 ## 0.7.1.2 (2020-03-08)
 * Minor fix for `kmsKey` ARN type under `imagebuilder` service. Fixes #114
 * Added PyInvoke command for updating the aws documentation.
diff --git a/examples/yml/crud-rds-mult.yml b/examples/yml/crud-rds-mult.yml
new file mode 100644
index 00000000..d273146a
--- /dev/null
+++ b/examples/yml/crud-rds-mult.yml
@@ -0,0 +1,7 @@
+mode: crud
+read:
+- 'arn:aws:rds:us-east-1:123456789012:db:mydatabase'
+write:
+- 'arn:aws:rds:us-east-1:123456789012:db:mydatabase'
+list:
+- 'arn:aws:rds:us-east-1:123456789012:db:mydatabase'
diff --git a/examples/yml/crud-rds-read.yml b/examples/yml/crud-rds-read.yml
new file mode 100644
index 00000000..c6bc05fb
--- /dev/null
+++ b/examples/yml/crud-rds-read.yml
@@ -0,0 +1,3 @@
+mode: crud
+read:
+- 'arn:aws:rds:us-east-1:123456789012:db:mydatabase'
diff --git a/policy_sentry/bin/policy_sentry b/policy_sentry/bin/policy_sentry
index 146f7107..e3552d1a 100755
--- a/policy_sentry/bin/policy_sentry
+++ b/policy_sentry/bin/policy_sentry
@@ -2,7 +2,7 @@
 """
     Policy Sentry is a tool for generating least-privilege IAM Policies.
 """
-__version__ = '0.7.1.2'
+__version__ = '0.7.1.3'
 import click
 from policy_sentry import command
 
diff --git a/policy_sentry/shared/data/audit/privilege-escalation.txt b/policy_sentry/shared/data/audit/privilege-escalation.txt
index fbf2b9ac..929a2299 100644
--- a/policy_sentry/shared/data/audit/privilege-escalation.txt
+++ b/policy_sentry/shared/data/audit/privilege-escalation.txt
@@ -32,3 +32,4 @@ lambda:UpdateFunctionConfiguration
 sagemaker:CreatePresignedNotebookInstanceUrl
 sagemaker:CreateNotebookInstance
 sts:AssumeRole
+rds:AddRoleToDBInstance
diff --git a/policy_sentry/util/arns.py b/policy_sentry/util/arns.py
index 920a0cd7..99a7113e 100644
--- a/policy_sentry/util/arns.py
+++ b/policy_sentry/util/arns.py
@@ -1,6 +1,9 @@
 """
 Functions to use for parsing ARNs, matching ARN types, and getting the right fragment/component from an ARN string,
 """
+import logging
+import re
+logger = logging.getLogger(__name__)
 
 
 def parse_arn(arn):
@@ -101,24 +104,57 @@ def arn_has_colons(arn):
         return False
 
 
-def does_arn_match(arn_to_test, arn_in_database):
-    """Given two ARNs, determine if they match. The cases supported are outlined below.
-
-    Case 1: arn:partition:service:region:account-id:resource
-
-    Case 2: arn:partition:service:region:account-id:resourcetype/resource
-
-    Case 3: arn:partition:service:region:account-id:resourcetype/resource/qualifier
-
-    Case 4: arn:partition:service:region:account-id:resourcetype/resource:qualifier
+def get_resource_string(arn):
+    """
+    Given an ARN, return the string after the account ID, no matter the ARN format.
 
-    Case 5: arn:partition:service:region:account-id:resourcetype:resource
+    :param arn: arn:partition:service:region:account-id:resourcetype/resource
+    :return: resourcetype/resource
+    """
+    split_arn = arn.split(":")
+    resource_string = (":".join(split_arn[5:]))
+    return resource_string
 
-    Case 6: arn:partition:service:region:account-id:resourcetype:resource:qualifier
 
-    Source: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-arns
+# TODO: query all arns in the service then find the ARN that matches the pattern here. This will match for buckets, for example.
+# In the meantime, we have to skip this pylint check (consider this as tech debt)
+# pylint: disable=inconsistent-return-statements
+def parse_arn_for_resource_type(arn):
+    """
+    Parses the resource string (resourcetype/resource and other variants) and grab the resource type.
 
-    :param arn: ARN to parse
+    :param arn:
+    :return:
+    """
+    split_arn = arn.split(":")
+    # Resource string will equal:
+    #     Case 1: resource
+    #     Case 2: resourcetype/resource
+    #     Case 3: resourcetype/resource/qualifier
+    #     Case 4: resourcetype/resource:qualifier
+    #     Case 5: resourcetype:resource
+    #     Case 6: resourcetype:resource:qualifier
+    resource_string = (":".join(split_arn[5:]))
+    split_resource = re.split('/|:', resource_string)
+    if len(split_resource) == 1:
+        # logger.debug(f"split_resource length is 1: {str(split_resource)}")
+        pass
+    elif len(split_resource) > 1:
+        return split_resource[0]
+
+
+#     Case 1: arn:partition:service:region:account-id:resource
+#     Case 2: arn:partition:service:region:account-id:resourcetype/resource
+#     Case 3: arn:partition:service:region:account-id:resourcetype/resource/qualifier
+#     Case 4: arn:partition:service:region:account-id:resourcetype/resource:qualifier
+#     Case 5: arn:partition:service:region:account-id:resourcetype:resource
+#     Case 6: arn:partition:service:region:account-id:resourcetype:resource:qualifier
+#     Source: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-arns
+def does_arn_match(arn_to_test, arn_in_database):
+    """
+    Given two ARNs, determine if they have the same resource type.
+    :param arn_to_test: ARN provided by user
+    :param arn_in_database: Raw ARN that exists in the policy sentry database
     :return: result of whether or not the ARNs match
     """
     score = 0
@@ -126,26 +162,27 @@ def does_arn_match(arn_to_test, arn_in_database):
     # arn_to_test = 'arn:aws:ssm:us-east-1:123456789012:parameter/test'
     exclusion_list = ["${ObjectName}"]
     if arn_in_database == "*":
-        score += 10  # Exit in this scenario
+        return False
     else:
+        resource_string_arn_in_database = get_resource_string(arn_in_database)
+        resource_string_arn_to_test = get_resource_string(arn_to_test)
+        resource_type_arn_in_database = parse_arn_for_resource_type(arn_in_database)
+        resource_type_arn_to_test = parse_arn_for_resource_type(arn_to_test)
         if get_service_from_arn(arn_in_database) != get_service_from_arn(arn_to_test):
+            score += 10
+            return False
+        if resource_type_arn_in_database == resource_type_arn_to_test:
+            return True
+        else:
             score += 1
-        if arn_has_colons(arn_in_database) != arn_has_colons(arn_to_test):
-            score += 1
-        if arn_has_slash(arn_in_database) != arn_has_slash(arn_to_test):
-            score += 1
-        if arn_has_slash(arn_in_database) and arn_has_slash(arn_to_test):
+        if resource_string_arn_in_database.count("/") > 0 and resource_string_arn_to_test.count("/") > 0:
             # Example: SSM `parameter/`
             if get_resource_from_arn(arn_in_database) != get_resource_from_arn(
                 arn_to_test
             ):
-
                 # Some exclusions, like ObjectId for S3 buckets
                 if get_resource_path_from_arn(arn_in_database) in exclusion_list:
-                    pass
+                    return True
                 else:
-                    score += 1
-
-    # logger.debug("Score is " + str(score))
-    # It passes if the alarm does not ring
+                    return False
     return score < 1
diff --git a/policy_sentry/writing/sid_group.py b/policy_sentry/writing/sid_group.py
index 8f8e4933..24b0dcce 100644
--- a/policy_sentry/writing/sid_group.py
+++ b/policy_sentry/writing/sid_group.py
@@ -94,11 +94,18 @@ class SidGroup:
         for sid in self.sids:
             actions = self.sids[sid]["actions"]
             if len(actions) == 0:
+                logger.debug(f"No actions for sid {sid}")
                 continue
             if minimize is not None and isinstance(minimize, int):
+                logger.debug("Minimizing statements...")
                 actions = minimize_statement_actions(
                     actions, all_actions, minchars=minimize
                 )
+            logger.debug(f"Adding statement with SID {sid}")
+            logger.debug(f"{sid} SID has the actions: {actions}")
+            logger.debug(f"{sid} SID has the resources: {self.sids[sid]['arn']}")
+
+
             statements.append(
                 {
                     "Sid": sid,
@@ -209,7 +216,9 @@ class SidGroup:
         dependent_actions = get_dependent_actions(db_session, supplied_actions)
         # List comprehension to get all dependent actions that are not in the supplied actions.
         dependent_actions = [x for x in dependent_actions if x not in supplied_actions]
-
+        logger.debug("Adding by list of actions")
+        logger.debug(f"Supplied actions: {str(supplied_actions)}")
+        logger.debug(f"Dependent actions: {str(dependent_actions)}")
         arns_matching_supplied_actions = []
 
         # arns_matching_supplied_actions is a list of dicts.
@@ -267,10 +276,16 @@ class SidGroup:
                 # self.add_action_without_resource_constraint(str.lower(dep_action))
         # Now, because add_by_arn_and_access_level() adds all actions under an access level, we have to
         # remove all actions that do not match the supplied_actions. This is done in-place.
+        logger.debug("Purging actions that do not match the requested actions and dependent actions")
+        logger.debug(f"Supplied actions: {str(supplied_actions)}")
+        logger.debug(f"Dependent actions: {str(dependent_actions)}")
         self.remove_actions_not_matching_these(supplied_actions + dependent_actions)
         for action in actions_without_resource_constraints:
+            logger.debug(f"Deliberately adding the action {action} without resource constraints")
             self.add_action_without_resource_constraint(action)
+        logger.debug("Removing actions that are in the wildcard arn (Resources = '*') as well as other statements that have resource constraints")
         self.remove_actions_duplicated_in_wildcard_arn()
+        logger.debug("Getting the rendered policy")
         rendered_policy = self.get_rendered_policy(db_session)
         return rendered_policy
 
@@ -285,6 +300,7 @@ class SidGroup:
         # try:
         if "mode" in cfg.keys():
             if cfg["mode"] == "crud":
+                logger.debug("CRUD mode selected")
                 check_crud_schema(cfg)
                 if "wildcard-only" in cfg.keys():
                     if "single-actions" in cfg["wildcard-only"]:
@@ -293,6 +309,7 @@ class SidGroup:
                                 provided_wildcard_actions = cfg["wildcard-only"][
                                     "single-actions"
                                 ]
+                                logger.debug(f"Requested wildcard-only actions: {str(provided_wildcard_actions)}")
                                 self.add_wildcard_only_actions(
                                     db_session, provided_wildcard_actions
                                 )
@@ -300,6 +317,7 @@ class SidGroup:
                         if cfg["wildcard-only"]["service-read"]:
                             if cfg["wildcard-only"]["service-read"][0] != "":
                                 service_read = cfg["wildcard-only"]["service-read"]
+                                logger.debug(f"Requested wildcard-only actions: {str(service_read)}")
                                 self.add_wildcard_only_actions_matching_services_and_access_level(
                                     db_session, service_read, "Read"
                                 )
@@ -307,6 +325,7 @@ class SidGroup:
                         if cfg["wildcard-only"]["service-write"]:
                             if cfg["wildcard-only"]["service-write"][0] != "":
                                 service_write = cfg["wildcard-only"]["service-write"]
+                                logger.debug(f"Requested wildcard-only actions: {str(service_write)}")
                                 self.add_wildcard_only_actions_matching_services_and_access_level(
                                     db_session, service_write, "Write"
                                 )
@@ -314,6 +333,7 @@ class SidGroup:
                         if cfg["wildcard-only"]["service-list"]:
                             if cfg["wildcard-only"]["service-list"][0] != "":
                                 service_list = cfg["wildcard-only"]["service-list"]
+                                logger.debug(f"Requested wildcard-only actions: {str(service_list)}")
                                 self.add_wildcard_only_actions_matching_services_and_access_level(
                                     db_session, service_list, "List"
                                 )
@@ -323,6 +343,7 @@ class SidGroup:
                                 service_tagging = cfg["wildcard-only"][
                                     "service-tagging"
                                 ]
+                                logger.debug(f"Requested wildcard-only actions: {str(service_tagging)}")
                                 self.add_wildcard_only_actions_matching_services_and_access_level(
                                     db_session, service_tagging, "Tagging"
                                 )
@@ -334,9 +355,11 @@ class SidGroup:
                                 ]
                                 != ""
                             ):
+
                                 service_permissions_management = cfg["wildcard-only"][
                                     "service-permissions-management"
                                 ]
+                                logger.debug(f"Requested wildcard-only actions: {str(service_permissions_management)}")
                                 self.add_wildcard_only_actions_matching_services_and_access_level(
                                     db_session,
                                     service_permissions_management,
@@ -344,16 +367,19 @@ class SidGroup:
                                 )
                 if "read" in cfg.keys():
                     if cfg["read"] is not None and cfg["read"][0] != "":
+                        logger.debug(f"Requested access to arns: {str(cfg['read'])}")
                         self.add_by_arn_and_access_level(
                             db_session, cfg["read"], "Read"
                         )
                 if "write" in cfg.keys():
                     if cfg["write"] is not None and cfg["write"][0] != "":
+                        logger.debug(f"Requested access to arns: {str(cfg['write'])}")
                         self.add_by_arn_and_access_level(
                             db_session, cfg["write"], "Write"
                         )
                 if "list" in cfg.keys():
                     if cfg["list"] is not None and cfg["list"][0] != "":
+                        logger.debug(f"Requested access to arns: {str(cfg['list'])}")
                         self.add_by_arn_and_access_level(
                             db_session, cfg["list"], "List"
                         )
@@ -362,6 +388,7 @@ class SidGroup:
                         cfg["permissions-management"] is not None
                         and cfg["permissions-management"][0] != ""
                     ):
+                        logger.debug(f"Requested access to arns: {str(cfg['permissions-management'])}")
                         self.add_by_arn_and_access_level(
                             db_session,
                             cfg["permissions-management"],
@@ -369,6 +396,7 @@ class SidGroup:
                         )
                 if "tagging" in cfg.keys():
                     if cfg["tagging"] is not None and cfg["tagging"][0] != "":
+                        logger.debug(f"Requested access to arns: {str(cfg['tagging'])}")
                         self.add_by_arn_and_access_level(
                             db_session, cfg["tagging"], "Tagging"
                         )
@@ -394,11 +422,9 @@ class SidGroup:
                 db_session, provided_wildcard_actions
             )
             if len(verified_wildcard_actions) > 0:
+                logger.debug("Attempting to add the following actions to the policy: %s", verified_wildcard_actions)
                 self.add_by_list_of_actions(db_session, verified_wildcard_actions)
-                logger.debug(
-                    "Added the following wildcard-only actions to the policy: %s",
-                    verified_wildcard_actions,
-                )
+                logger.debug("Added the following wildcard-only actions to the policy: %s", verified_wildcard_actions)
 
     def add_wildcard_only_actions_matching_services_and_access_level(
         self, db_session, services, access_level
diff --git a/utils/download_docs.py b/utils/download_docs.py
index 930a7386..bc593b8e 100755
--- a/utils/download_docs.py
+++ b/utils/download_docs.py
@@ -106,7 +106,6 @@ def write_arn_table_csv(db_session):
 
 
 def write_condition_table_csv(db_session):
-    print()
     rows = db_session.query(ConditionTable)
     f = open(os.path.join(BASE_DIR, 'policy_sentry/shared/data', 'condition_table.csv'), 'w')
     out = csv.writer(f, delimiter=';')


Test patch:
diff --git a/docs/contributing/testing.rst b/docs/contributing/testing.rst
index 8e25b2f4..676869c1 100644
--- a/docs/contributing/testing.rst
+++ b/docs/contributing/testing.rst
@@ -97,6 +97,12 @@ We use `Nose <https://nose.readthedocs.io/en/latest/>`_ for unit testing. All te
 
     nosetests -v
 
+    # This will output the print() statements in your test code
+    nosetests -v --nocapture
+
+    # This will include the debug logging statements in the test output
+    nosetests -v --logging-level=DEBUG
+
 
 * Alternatively, you can use `invoke`, as mentioned above:
 
diff --git a/test/command/test_write_policy.py b/test/command/test_write_policy.py
index 4b2453c6..629cf597 100644
--- a/test/command/test_write_policy.py
+++ b/test/command/test_write_policy.py
@@ -123,3 +123,110 @@ class WildcardOnlyServiceLevelTestCase(unittest.TestCase):
             ]
         }
         self.assertDictEqual(output, desired_output)
+
+
+class RdsWritingTestCase(unittest.TestCase):
+    def test_rds_policy_read_only(self):
+        """test_rds_policy_read_only: Make sure that the RDS Policies work properly"""
+        policy_file_path = abspath(
+            join(
+                dirname(__file__), pardir + "/" + pardir + "/examples/yml/crud-rds-read.yml",
+            )
+        )
+        cfg = read_yaml_file(policy_file_path)
+        desired_output = {
+            "Version": "2012-10-17",
+            "Statement": [
+                {
+                    "Sid": "RdsReadDb",
+                    "Effect": "Allow",
+                    "Action": [
+                        "rds:DownloadDBLogFilePortion",
+                        "rds:ListTagsForResource"
+                    ],
+                    "Resource": [
+                        "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
+                    ]
+                }
+            ]
+        }
+        policy = write_policy_with_template(db_session, cfg)
+        print(json.dumps(policy, indent=4))
+        self.assertDictEqual(desired_output, policy)
+
+    def test_rds_policy_read_write_list(self):
+        """test_rds_policy_read_write_list: Make sure that the RDS Policies work properly for multiple levels"""
+        policy_file_path = abspath(
+            join(
+                dirname(__file__), pardir + "/" + pardir + "/examples/yml/crud-rds-mult.yml",
+            )
+        )
+        cfg = read_yaml_file(policy_file_path)
+        desired_output = {
+            "Version": "2012-10-17",
+            "Statement": [
+                {
+                    "Sid": "RdsReadDb",
+                    "Effect": "Allow",
+                    "Action": [
+                        "rds:DownloadDBLogFilePortion",
+                        "rds:ListTagsForResource"
+                    ],
+                    "Resource": [
+                        "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
+                    ]
+                },
+                {
+                    "Sid": "MultMultNone",
+                    "Effect": "Allow",
+                    "Action": [
+                        "iam:PassRole"
+                    ],
+                    "Resource": [
+                        "*"
+                    ]
+                },
+                {
+                    "Sid": "RdsWriteDb",
+                    "Effect": "Allow",
+                    "Action": [
+                        "rds:AddRoleToDBInstance",
+                        "rds:ApplyPendingMaintenanceAction",
+                        "rds:CreateDBInstance",
+                        "rds:CreateDBInstanceReadReplica",
+                        "rds:CreateDBSnapshot",
+                        "rds:DeleteDBInstance",
+                        "rds:DeregisterDBProxyTargets",
+                        "rds:ModifyDBInstance",
+                        "rds:PromoteReadReplica",
+                        "rds:RebootDBInstance",
+                        "rds:RemoveRoleFromDBInstance",
+                        "rds:RestoreDBInstanceFromDBSnapshot",
+                        "rds:RestoreDBInstanceFromS3",
+                        "rds:RestoreDBInstanceToPointInTime",
+                        "rds:StartDBInstance",
+                        "rds:StopDBInstance"
+                    ],
+                    "Resource": [
+                        "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
+                    ]
+                },
+                {
+                    "Sid": "RdsListDb",
+                    "Effect": "Allow",
+                    "Action": [
+                        "rds:DescribeDBLogFiles",
+                        "rds:DescribeDBProxyTargets",
+                        "rds:DescribeDBSnapshots",
+                        "rds:DescribePendingMaintenanceActions",
+                        "rds:DescribeValidDBInstanceModifications"
+                    ],
+                    "Resource": [
+                        "arn:aws:rds:us-east-1:123456789012:db:mydatabase"
+                    ]
+                }
+            ]
+        }
+        policy = write_policy_with_template(db_session, cfg)
+        print(json.dumps(policy, indent=4))
+        self.assertDictEqual(desired_output, policy)
diff --git a/test/util/test_arns.py b/test/util/test_arns.py
index f54f5c8b..d9520fad 100644
--- a/test/util/test_arns.py
+++ b/test/util/test_arns.py
@@ -2,7 +2,6 @@ import unittest
 from policy_sentry.util.arns import does_arn_match
 from policy_sentry.shared.database import connect_db
 from policy_sentry.shared.constants import DATABASE_FILE_PATH
-
 db_session = connect_db(DATABASE_FILE_PATH)
 
 # "Does Arn Match" tests
@@ -15,7 +14,7 @@ db_session = connect_db(DATABASE_FILE_PATH)
 # Case 4: arn:partition:service:region:account-id:resourcetype/resource:qualifier
 # Case 5: arn:partition:service:region:account-id:resourcetype:resource
 # Case 6: arn:partition:service:region:account-id:resourcetype:resource:qualifier
-
+# Case 7: arn:partition:service:region:account-id:/greengrass/definition/devices/${DeviceDefinitionId}/versions/${VersionId}
 
 class ArnsTestCase(unittest.TestCase):
     def test_does_arn_match_case_bucket(self):
@@ -66,8 +65,40 @@ class ArnsTestCase(unittest.TestCase):
         arn_in_database = "arn:${Partition}:states:${Region}:${Account}:execution:${StateMachineName}:${ExecutionId}"
         self.assertTrue(does_arn_match(arn_to_test, arn_in_database))
 
-    # def test_does_arn_match_case_greengrass(self):
-    #     # Undocumented case: AWS Greengrass: arn:aws:greengrass:${Region}:${Account}:/greengrass/definition/devices/${DeviceDefinitionId}/versions/${VersionId}
-    #     arn_to_test = "arn:aws:greengrass:${Region}:${Account}:/greengrass/definition/devices/1234567}/versions/1"
-    #     arn_in_database = "arn:aws:greengrass:${Region}:${Account}:/greengrass/definition/devices/${DeviceDefinitionId}/versions/${VersionId}"
-    #     self.assertTrue(does_arn_match(arn_to_test, arn_in_database))
+    def test_does_arn_match_case_greengrass(self):
+        # Undocumented case: AWS Greengrass: arn:aws:greengrass:${Region}:${Account}:/greengrass/definition/devices/${DeviceDefinitionId}/versions/${VersionId}
+        arn_to_test = "arn:aws:greengrass:${Region}:${Account}:/greengrass/definition/devices/1234567}/versions/1"
+        arn_in_database = "arn:aws:greengrass:${Region}:${Account}:/greengrass/definition/devices/${DeviceDefinitionId}/versions/${VersionId}"
+        self.assertTrue(does_arn_match(arn_to_test, arn_in_database))
+
+    def test_does_arn_match_rds(self):
+        arns_to_test = [
+            "arn:${Partition}:rds:${Region}:${Account}:cluster:${DbClusterInstanceName}",
+            "arn:${Partition}:rds:${Region}:${Account}:cluster-endpoint:${DbClusterEndpoint}",
+            "arn:${Partition}:rds:${Region}:${Account}:cluster-pg:${ClusterParameterGroupName}",
+            "arn:${Partition}:rds:${Region}:${Account}:cluster-snapshot:${ClusterSnapshotName}",
+            "arn:${Partition}:rds:${Region}:${Account}:es:${SubscriptionName}",
+            "arn:${Partition}:rds:${Account}:global-cluster:${GlobalCluster}",
+            "arn:${Partition}:rds:${Region}:${Account}:og:${OptionGroupName}",
+            "arn:${Partition}:rds:${Region}:${Account}:pg:${ParameterGroupName}",
+            "arn:${Partition}:rds:${Region}:${Account}:db-proxy:${DbProxyId}",
+            "arn:${Partition}:rds:${Region}:${Account}:ri:${ReservedDbInstanceName}",
+            "arn:${Partition}:rds:${Region}:${Account}:secgrp:${SecurityGroupName}",
+            "arn:${Partition}:rds:${Region}:${Account}:snapshot:${SnapshotName}",
+            "arn:${Partition}:rds:${Region}:${Account}:subgrp:${SubnetGroupName}",
+            "arn:${Partition}:rds:${Region}:${Account}:target:${TargetId}",
+            "arn:${Partition}:rds:${Region}:${Account}:target-group:${TargetGroupId}"
+        ]
+        arn_in_database = "arn:${Partition}:rds:${Region}:${Account}:db:${DbInstanceName}"
+        for arn in arns_to_test:
+            decision = does_arn_match(arn, arn_in_database)
+            self.assertFalse(decision)
+
+        arn_to_test = "arn:${Partition}:rds:${Region}:${Account}:cluster:${DbClusterInstanceName}"
+        decision = does_arn_match(arn_to_test, arn_in_database)
+        self.assertFalse(decision)
+
+        arn_to_test = "arn:${Partition}:rds:${Region}:${Account}:db:${DbInstanceName}"
+        arn_in_database = "arn:${Partition}:rds:${Region}:${Account}:db:${DbInstanceName}"
+        decision = does_arn_match(arn_to_test, arn_in_database)
+        self.assertTrue(decision)