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)
def test_dynamodb_arn_matching_gh_215(self): """test_dynamodb_arn_matching_gh_215: Validate fix for DynamoDB arn mismatch in GitHub issue #215""" index = "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}/index/${IndexName}" stream = "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}/stream/${StreamLabel}" table = "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}" backup = "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}/backup/${BackupName}" global_table = "arn:${Partition}:dynamodb::${Account}:global-table/${GlobalTableName}" this_arn = ARN("arn:aws:dynamodb:us-east-1:123456789123:table/mytable") self.assertTrue(this_arn.same_resource_type(table)) result = this_arn.same_resource_type(index) self.assertFalse(result) result = this_arn.same_resource_type(stream) self.assertFalse(result) result = this_arn.same_resource_type(backup) self.assertFalse(result) result = this_arn.same_resource_type(global_table) self.assertFalse(result) this_arn = "arn:aws:dynamodb:us-east-1:123456789123:table/mytable" self.assertTrue(does_arn_match(this_arn, table)) self.assertFalse(does_arn_match(this_arn, index)) self.assertFalse(does_arn_match(this_arn, stream)) self.assertFalse(does_arn_match(this_arn, backup)) self.assertFalse(does_arn_match(this_arn, global_table))
def test_does_arn_match_resource_wildcard(self): arn_to_test = "arn:${Partition}:rds:${Region}:${Account}:*:*" arn_in_database = "arn:${Partition}:rds:${Region}:${Account}:db:${DbInstanceName}" decision = does_arn_match(arn_to_test, arn_in_database) self.assertTrue(decision) # Make sure wrong service yields False arn_to_test = "arn:${Partition}:s3:${Region}:${Account}:*:*" arn_in_database = "arn:${Partition}:rds:${Region}:${Account}:db:${DbInstanceName}" decision = does_arn_match(arn_to_test, arn_in_database) self.assertFalse(decision)
def add_by_arn_and_access_level(self, arn_list, access_level, conditions_block=None): """ This adds the user-supplied ARN(s), service prefixes, access levels, and condition keys (if applicable) given by the user. It derives the list of IAM actions based on the user's requested ARNs and access levels. Arguments: arn_list: Just a list of resource ARNs. access_level: "Read", "List", "Tagging", "Write", or "Permissions management" conditions_block: Optionally, a condition block with one or more conditions """ for arn in arn_list: service_prefix = get_service_from_arn(arn) service_action_data = get_action_data(service_prefix, "*") for service_prefix in service_action_data: for row in service_action_data[service_prefix]: if (does_arn_match(arn, row["resource_arn_format"]) and row["access_level"] == access_level): raw_arn_format = row["resource_arn_format"] resource_type_name = get_resource_type_name_with_raw_arn( raw_arn_format) sid_namespace = create_policy_sid_namespace( service_prefix, access_level, resource_type_name) actions = get_actions_with_arn_type_and_access_level( service_prefix, resource_type_name, access_level) # Make supplied actions lowercase # supplied_actions = [x.lower() for x in actions] supplied_actions = actions.copy() dependent_actions = get_dependent_actions( 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 ] if len(dependent_actions) > 0: for dep_action in dependent_actions: self.add_action_without_resource_constraint( dep_action) # self.add_action_without_resource_constraint( # str.lower(dep_action) # ) temp_sid_dict = { "arn": [arn], "service": service_prefix, "access_level": access_level, "arn_format": raw_arn_format, "actions": actions, "conditions": [], # TODO: Add conditions } if sid_namespace in self.sids.keys(): # If the ARN already exists there, skip it. if arn not in self.sids[sid_namespace]["arn"]: self.sids[sid_namespace]["arn"].append(arn) # If it did not exist before at all, create it. else: self.sids[sid_namespace] = temp_sid_dict
def test_does_arn_match_case_6(self): # Case 6: arn:partition:service:region:account-id:resourcetype:resource:qualifier arn_to_test = ( "arn:aws:states:region:account-id:execution:stateMachineName:executionName" ) 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_1(self): # Case 1: arn:partition:service:region:account-id:resource arn_to_test = "arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo" arn_in_database = ( "arn:${Partition}:codecommit:${Region}:${Account}:${RepositoryName}" ) self.assertTrue(does_arn_match(arn_to_test, arn_in_database))
def add(self, db_session, arn_list_from_user, access_level): """ This just adds the ARN, Service, and Access Level. ARN Format and Actions are not filled out. Example data can be found in the class ArnActionGroupTestCase in the testing folder. :param db_session: SQLAlchemy database session :param arn_list_from_user: Just a list of resource ARNs. :param access_level: "Read", "List", "Tagging", "Write", or "Permissions management" """ for arn_from_user in arn_list_from_user: service = get_service_from_arn(arn_from_user) for row in db_session.query(ActionTable).filter( ActionTable.service.like(service)): if does_arn_match(arn_from_user, row.resource_arn_format): if row.access_level == access_level: # If it's not a key in the dictionary, add it as a key # and then add the item in the list raw_arn_format = row.resource_arn_format temp_arn_dict = { 'arn': arn_from_user, 'service': service, 'access_level': access_level, 'arn_format': raw_arn_format, 'actions': [] } # If there is already an entry, skip it to avoid duplicates # Otherwise, add it if temp_arn_dict in self.arns: continue self.arns.append(copy.deepcopy(temp_arn_dict))
def add_sts_actions(self, sts_actions): """ To add STS actions to the output from special YAML section """ if sts_actions: # Hard coded for this special case service_prefix = "sts" access_level = "Write" for action, arns in sts_actions.items(): clean_action = action.replace( '-', '' ) # Convention to follow adding dashes instead of CamelCase service_action_data = get_action_data(service_prefix, clean_action) # Schema validation takes care of this, but just in case. No data returned for the action if not service_action_data: raise Exception( f"Could not find service action data for {service_prefix} - {clean_action}" ) for row in service_action_data[service_prefix]: for arn in arns: if not arn: # skip the - '' situation continue if (does_arn_match(arn, row["resource_arn_format"]) and row["access_level"] == access_level): raw_arn_format = row["resource_arn_format"] # Each action will get its own namespace sts:AssumeRole -> AssumeRole # -1 index is a neat trick if the colon ever goes away we won't get an index error. sid_namespace = row["action"].split(':')[-1] temp_sid_dict = { "arn": [arn], "service": service_prefix, "access_level": access_level, "arn_format": raw_arn_format, "actions": [row["action"]], "conditions": [], # TODO: Add conditions } # Using a custom namespace and not gathering actions so no need to find # dependent actions either, though we could do it here if sid_namespace in self.sids.keys(): # If the ARN already exists there, skip it. if arn not in self.sids[sid_namespace]["arn"]: self.sids[sid_namespace]["arn"].append(arn) else: self.sids[sid_namespace] = temp_sid_dict
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
def get_matching_raw_arns(arn): """ Given a user-supplied ARN, return the list of raw_arns since that is used as a unique identifier throughout this library Arguments: arn: The user-supplied arn, like arn:aws:s3:::mybucket Returns: list(str): The list of raw ARNs stored in the database, like 'arn:${Partition}:s3:::${BucketName}' """ result = [] 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) and raw_arn not in result: result.append(raw_arn) return result
def test_does_arn_match_case_5(self): # Case 5: arn:partition:service:region:account-id:resourcetype:resource arn_to_test = "arn:aws:states:region:account-id:stateMachine:stateMachineName" arn_in_database = "arn:${Partition}:states:${Region}:${Account}:stateMachine:${StateMachineName}" self.assertTrue(does_arn_match(arn_to_test, arn_in_database))
def test_does_arn_match_case_4(self): # Case 4: arn:partition:service:region:account-id:resourcetype/resource:qualifier arn_to_test = "arn:aws:batch:region:account-id:job-definition/job-name:revision" arn_in_database = "arn:${Partition}:batch:${Region}:${Account}:job-definition/${JobDefinitionName}:${Revision}" self.assertTrue(does_arn_match(arn_to_test, arn_in_database))
def test_does_arn_match_case_2(self): # Case 2: arn:partition:service:region:account-id:resourcetype/resource arn_to_test = "arn:aws:ssm:us-east-1:123456789012:parameter/test" arn_in_database = "arn:${Partition}:ssm:${Region}:${Account}:parameter/${FullyQualifiedParameterName}" 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_bucket(self): # Case 1: arn:partition:service:region:account-id:resource arn_to_test = "arn:aws:s3:::bucket_name" arn_in_database = "arn:${Partition}:s3:::${BucketName}" self.assertTrue(does_arn_match(arn_to_test, arn_in_database))
def test_dynamo_db_non_paths(self): backup_arn = "arn:aws:dynamodb:us-east-1:123456789123:table/mytable/backup/mybackup" backup_raw_arn = "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}/backup/${BackupName}" table_arn = "arn:aws:dynamodb:us-east-1:123456789123:table/mytable" table_raw_arn = "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}" parameter_arn_with_path = "arn:aws:ssm:::parameter/dev/foo/bar*" parameter_arn_without_path = "arn:aws:ssm:::parameter/dev" parameter_raw_arn = "arn:${Partition}:ssm:${Region}:${Account}:parameter/${FullyQualifiedParameterName}" s3_object_with_path = "arn:aws:s3:::foo/bar/baz" s3_object_without_path = "arn:aws:s3:::foo/bar" s3_object_raw_arn = "arn:${Partition}:s3:::${BucketName}/${ObjectName}" s3_bucket_raw_arn = "arn:${Partition}:s3:::${BucketName}" ecr_raw_arn = "arn:${Partition}:ecr:${Region}:${Account}:repository/${RepositoryName}" ecr_arn_with_path = "arn:aws:ecr:*:*:repository/foo/bar" ecr_arn_without_path = "arn:aws:ecr:*:*:repository/foo" self.assertTrue(does_arn_match(backup_arn, backup_raw_arn)) self.assertTrue(does_arn_match(table_arn, table_raw_arn)) self.assertFalse(does_arn_match(table_arn, backup_raw_arn)) self.assertFalse(does_arn_match(backup_arn, table_raw_arn)) self.assertTrue( does_arn_match(parameter_arn_with_path, parameter_raw_arn)) self.assertTrue( does_arn_match(parameter_arn_without_path, parameter_raw_arn)) self.assertTrue( does_arn_match(s3_object_without_path, s3_object_raw_arn)) self.assertTrue(does_arn_match(s3_object_with_path, s3_object_raw_arn)) self.assertFalse(does_arn_match(s3_object_with_path, s3_bucket_raw_arn)) self.assertFalse( does_arn_match(s3_object_without_path, s3_bucket_raw_arn)) self.assertTrue(does_arn_match(ecr_arn_with_path, ecr_raw_arn)) self.assertTrue(does_arn_match(ecr_arn_without_path, ecr_raw_arn))
#!/usr/bin/env python from policy_sentry.util.arns import does_arn_match if __name__ == '__main__': print( does_arn_match("arn:aws:s3:::bucket_name", "arn:${Partition}:s3:::${BucketName}")) print( does_arn_match( "arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo", "arn:${Partition}:codecommit:${Region}:${Account}:${RepositoryName}" )) print( does_arn_match( "arn:aws:ssm:us-east-1:123456789012:parameter/test", "arn:${Partition}:ssm:${Region}:${Account}:parameter/${FullyQualifiedParameterName}" )) print( does_arn_match( "arn:aws:batch:region:account-id:job-definition/job-name:revision", "arn:${Partition}:batch:${Region}:${Account}:job-definition/${JobDefinitionName}:${Revision}" )) print( does_arn_match( "arn:aws:states:region:account-id:stateMachine:stateMachineName", "arn:${Partition}:states:${Region}:${Account}:stateMachine:${StateMachineName}" )) print( does_arn_match( "arn:aws:states:region:account-id:execution:stateMachineName:executionName", "arn:${Partition}:states:${Region}:${Account}:execution:${StateMachineName}:${ExecutionId}"