Repository: salesforce/policy_sentry
Pull request #170 (2020-04-20 22:23:43)

Add two new query functions
* Added `get_matching_raw_arn` query function (#169)
* Added `get_actions_matching_arn` query function


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b235be6..288a9bc8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 ## Future release (Unreleased)
 * Add conditions support
 
+## 0.8.0.4 (2020-04-20)
+* Added `get_matching_raw_arn` query function (#169)
+* Added `get_actions_matching_arn` query function
+
 ## 0.8.0.3 (2020-04-16)
 * Added logging for Query functions (#161)
 * Added `get_expanded_policy` function and moved some of the `analysis.analyze` functions to `analysis.expand` (Fixes #164)
diff --git a/policy_sentry/bin/cli.py b/policy_sentry/bin/cli.py
index 4784effe..ca5e98af 100755
--- a/policy_sentry/bin/cli.py
+++ b/policy_sentry/bin/cli.py
@@ -2,7 +2,7 @@
 """
     Policy Sentry is a tool for generating least-privilege IAM Policies.
 """
-__version__ = "0.8.0.3"
+__version__ = "0.8.0.4"
 import click
 from policy_sentry import command
 
diff --git a/policy_sentry/querying/actions.py b/policy_sentry/querying/actions.py
index 13631695..780440a7 100644
--- a/policy_sentry/querying/actions.py
+++ b/policy_sentry/querying/actions.py
@@ -5,6 +5,8 @@ This supports the Policy Sentry query functionality
 import logging
 from policy_sentry.shared.iam_data import iam_definition, get_service_prefix_data
 from policy_sentry.querying.all import get_all_service_prefixes
+from policy_sentry.querying.arns import get_matching_raw_arn, get_resource_type_name_with_raw_arn
+from policy_sentry.util.arns import get_service_from_arn
 
 all_service_prefixes = get_all_service_prefixes()
 logger = logging.getLogger(__name__)
@@ -189,6 +191,23 @@ def get_actions_with_arn_type_and_access_level(
     return results
 
 
+def get_actions_matching_arn(arn):
+    """Given a user-supplied arn, get a list of all actions that can match it."""
+    raw_arn = get_matching_raw_arn(arn)
+    resource_type_name = get_resource_type_name_with_raw_arn(raw_arn)
+    service_prefix = get_service_from_arn(raw_arn)
+    service_prefix_data = get_service_prefix_data(service_prefix)
+    results = []
+    for some_action in service_prefix_data["privileges"]:
+        for some_resource_type in some_action["resource_types"]:
+            this_resource_type = some_resource_type["resource_type"].strip("*")
+            if this_resource_type.lower() == resource_type_name.lower():
+                results.append(f"{service_prefix}:{some_action['privilege']}")
+    results = list(dict.fromkeys(results))
+    results.sort()
+    return results
+
+
 def get_actions_matching_condition_key(service_prefix, condition_key):
     """
     Get a list of actions under a service that allow the use of a specified condition key
diff --git a/policy_sentry/querying/arns.py b/policy_sentry/querying/arns.py
index 1b7bab3b..689e2c46 100644
--- a/policy_sentry/querying/arns.py
+++ b/policy_sentry/querying/arns.py
@@ -4,6 +4,7 @@ This supports the policy_sentry query functionality
 """
 import logging
 from policy_sentry.shared.iam_data import iam_definition, get_service_prefix_data
+from policy_sentry.util.arns import does_arn_match, get_service_from_arn
 
 logger = logging.getLogger(__name__)
 
@@ -96,3 +97,21 @@ def get_resource_type_name_with_raw_arn(raw_arn):
     for resource in service_data["resources"]:
         if resource["arn"].lower() == raw_arn.lower():
             return resource["resource"]
+
+
+def get_matching_raw_arn(arn):
+    """
+    Given a user-supplied ARN, return the raw_arn since that is used as a unique identifier throughout this library
+
+    :param arn: The user-supplied arn, like arn:aws:s3:::mybucket
+    :return: The raw ARN stored in the database, like 'arn:${Partition}:s3:::${BucketName}'
+    """
+    result = None
+    service_in_scope = get_service_from_arn(arn)
+    # Determine which resource it applies to
+    all_raw_arns_for_service = get_raw_arns_for_service(service_in_scope)
+    # Get the raw ARN specific to the provided one
+    for raw_arn in all_raw_arns_for_service:
+        if does_arn_match(arn, raw_arn):
+            result = raw_arn
+    return result


Test patch:
diff --git a/test/querying/test_query_actions.py b/test/querying/test_query_actions.py
index 29ff6ff7..df14fffc 100644
--- a/test/querying/test_query_actions.py
+++ b/test/querying/test_query_actions.py
@@ -15,6 +15,7 @@ from policy_sentry.querying.actions import (
     get_dependent_actions,
     remove_actions_that_are_not_wildcard_arn_only,
     get_actions_matching_condition_key,
+    get_actions_matching_arn
     # get_actions_matching_condition_crud_and_arn
 )
 from policy_sentry.writing.validate import check
@@ -388,3 +389,14 @@ class QueryActionsTestCase(unittest.TestCase):
         )
         self.maxDiff = None
         self.assertListEqual(desired_output, output)
+
+    def test_get_actions_matching_arn(self):
+        """querying.actions.get_actions_matching_arn"""
+        arn = "arn:aws:cloud9:us-east-1:account-id:environment:123456"
+        results = get_actions_matching_arn(arn)
+        # print(json.dumps(results, indent=4))
+        # Don't want to keep an updated list of actions in these tests,
+        # so let's just test the lengths and look for some contents that should or should not be in there.
+        self.assertTrue(len(results) > 10)
+        self.assertTrue("cloud9:ListEnvironments" not in results)
+        self.assertTrue("cloud9:DeleteEnvironment" in results)
diff --git a/test/querying/test_query_arns.py b/test/querying/test_query_arns.py
index c321a6a7..ca792975 100644
--- a/test/querying/test_query_arns.py
+++ b/test/querying/test_query_arns.py
@@ -6,7 +6,8 @@ from policy_sentry.querying.arns import (
     get_raw_arns_for_service,
     get_arn_types_for_service,
     get_arn_type_details,
-    get_resource_type_name_with_raw_arn
+    get_resource_type_name_with_raw_arn,
+    get_matching_raw_arn
 )
 
 
@@ -48,7 +49,6 @@ class QueryArnsTestCase(unittest.TestCase):
         self.maxDiff = None
         self.assertEqual(results, expected_results)
 
-
     def test_get_arn_type_details(self):
         """querying.arns.get_arn_type_details: Tests function that grabs details about a specific ARN name"""
         expected_results = {
@@ -64,3 +64,14 @@ class QueryArnsTestCase(unittest.TestCase):
         """querying.arns.get_resource_type_name_with_raw_arn"""
         raw_arn = "arn:${Partition}:cloud9:${Region}:${Account}:environment:${ResourceId}"
         self.assertTrue(get_resource_type_name_with_raw_arn(raw_arn), "environment")
+
+    def test_get_matching_raw_arn(self):
+        """querying.arns.get_matching_raw_arn"""
+        self.assertEqual(get_matching_raw_arn("arn:aws:s3:::bucket_name"), "arn:${Partition}:s3:::${BucketName}")
+        self.assertEqual(get_matching_raw_arn("arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo"), "arn:${Partition}:codecommit:${Region}:${Account}:${RepositoryName}")
+        self.assertEqual(get_matching_raw_arn("arn:aws:ssm:us-east-1:123456789012:parameter/test"), "arn:${Partition}:ssm:${Region}:${Account}:parameter/${FullyQualifiedParameterName}")
+        self.assertEqual(get_matching_raw_arn("arn:aws:batch:region:account-id:job-definition/job-name:revision"), "arn:${Partition}:batch:${Region}:${Account}:job-definition/${JobDefinitionName}:${Revision}")
+        self.assertEqual(get_matching_raw_arn("arn:aws:states:region:account-id:stateMachine:stateMachineName"), "arn:${Partition}:states:${Region}:${Account}:stateMachine:${StateMachineName}")
+        self.assertEqual(get_matching_raw_arn("arn:aws:states:region:account-id:execution:stateMachineName:executionName"), "arn:${Partition}:states:${Region}:${Account}:execution:${StateMachineName}:${ExecutionId}")
+        # self.assertEqual(get_matching_raw_arn("arn:aws:greengrass:region:account-id:/greengrass/definition/devices/1234567/versions/1"), "arn:aws:greengrass:${Region}:${Account}:/greengrass/definition/devices/${DeviceDefinitionId}/versions/${VersionId}")
+        self.assertEqual(get_matching_raw_arn("arn:${Partition}:rds:region:account-id:db:mydatabase"), "arn:${Partition}:rds:${Region}:${Account}:db:${DbInstanceName}")
diff --git a/test/util/test_arns.py b/test/util/test_arns.py
index 2636edc7..e3c6d999 100644
--- a/test/util/test_arns.py
+++ b/test/util/test_arns.py
@@ -64,7 +64,7 @@ class ArnsTestCase(unittest.TestCase):
 
     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_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))