def pull_resource_policy_by_arn(session: botocore.session.Session, arn: Optional[str], query: str = None) -> dict: """helper function for pulling the resource policy for a resource at the denoted ARN. raises ValueError if it cannot be retrieved, or a botocore ClientError if another issue arises """ if query is not None: if arn is not None: raise ValueError('Must specify either arn or query, not both.') pattern = re.compile(r'.*(arn:[^:]*:[^:]*:[^:]*:[^:]*:\S+).*') matches = pattern.match(query) if matches is None: raise ValueError('Resource policy retrieval error: could not extract resource ARN from query') arn = matches.group(1) if '?' in arn or '*' in arn: raise ValueError('Resource component from query must not have wildcard (? or *) when evaluating ' 'resource policies.') service = arns.get_service(arn) if service == 'iam': # arn:aws:iam::<account_id>:role/<role_name> client = session.create_client('iam') role_name = arns.get_resource(arn).split('/')[-1] logger.debug('Calling IAM API to retrieve AssumeRolePolicyDocument of {}'.format(role_name)) trust_doc = client.get_role(RoleName=role_name)['Role']['AssumeRolePolicyDocument'] return trust_doc elif service == 's3': # arn:aws:s3:::<bucket>/<path_to_object_with_potential_colons> client = session.create_client('s3') bucket_name = arns.get_resource(arn).split('arn:aws:s3:::')[-1].split('/')[0] logger.debug('Calling S3 API to retrieve bucket policy of {}'.format(bucket_name)) bucket_policy = json.loads(client.get_bucket_policy(Bucket=bucket_name)['Policy']) return bucket_policy elif service == 'sns': region = arns.get_region(arn) client = session.create_client('sns', region_name=region) logger.debug('Calling SNS API to retrieve topic policy of {}'.format(arn)) policy_str = client.get_topic_attributes(TopicArn=arn)['Attributes']['Policy'] return json.loads(policy_str) elif service == 'sqs': region = arns.get_region(arn) client = session.create_client('sqs', region_name=region) logger.debug('Calling SQS API to retrieve queue policy of {}'.format(arn)) queue_url = 'https://sqs.{}.amazonaws.com/{}/{}'.format( arns.get_region(arn), arns.get_account_id(arn), arns.get_resource(arn) ) # TODO: future proof queue URL creation? this still work with FIFO queues? policy_str = client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['Policy'])['Policy'] return json.loads(policy_str) elif service == 'kms': region = arns.get_region(arn) client = session.create_client('kms', region_name=region) logger.debug('Calling KMS API to retrieve key policy of {}'.format(arn)) key_policy = json.loads(client.get_key_policy(KeyId=arn, PolicyName='default')['Policy']) return key_policy
def process_arguments(parsed_args: Namespace): """Given a namespace object generated from parsing args, perform the appropriate tasks. Returns an int matching expectations set by /usr/include/sysexits.h for command-line utilities.""" if parsed_args.account is None: session = botocore_tools.get_session(parsed_args.profile) else: session = None graph = graph_actions.get_existing_graph(session, parsed_args.account) logger.debug('Querying against graph {}'.format(graph.metadata['account_id'])) if parsed_args.with_resource_policy: resource_policy = query_utils.pull_cached_resource_policy_by_arn( graph, arn=None, query=parsed_args.query ) elif parsed_args.resource_policy_text: resource_policy = json.loads(parsed_args.resource_policy_text) else: resource_policy = None resource_owner = parsed_args.resource_owner if resource_policy is not None: if resource_owner is None: if arns.get_service(resource_policy.arn) == 's3': raise ValueError('Must supply resource owner (--resource-owner) when including S3 bucket policies ' 'in a query') else: resource_owner = arns.get_account_id(resource_policy.arn) if isinstance(resource_policy, Policy): resource_policy = resource_policy.policy_doc if parsed_args.scps: if 'org-id' in graph.metadata and 'org-path' in graph.metadata: org_tree_path = os.path.join(get_storage_root(), graph.metadata['org-id']) org_tree = OrganizationTree.create_from_dir(org_tree_path) scps = query_orgs.produce_scp_list(graph, org_tree) else: raise ValueError('Graph for account {} does not have an associated OrganizationTree mapped (need to run ' '`pmapper orgs create/update` to get that.') else: scps = None query_actions.query_response( graph, parsed_args.query, parsed_args.skip_admin, resource_policy, resource_owner, parsed_args.include_unauthorized, parsed_args.session_policy, scps ) return 0
def pull_cached_resource_policy_by_arn(graph: Graph, arn: Optional[str], query: str = None) -> Union[Policy, dict]: """Function that pulls a resource policy that's cached on-disk from the given Graph object. Returns either a Policy object or a dictionary representing the resource policy. Caller is responsible for checking before sending it along to other components. Raises ValueError if it is not able to be retrieved. """ if query is not None: if arn is not None: raise ValueError('Must specify either arn or query, not both.') pattern = re.compile(r'.*(arn:[^:]*:[^:]*:[^:]*:[^:]*:\S+).*') matches = pattern.match(query) if matches is None: raise ValueError('Resource policy retrieval error: could not extract resource ARN from query') arn = matches.group(1) if '?' in arn or '*' in arn: raise ValueError('Resource component from query must not have wildcard (? or *) when evaluating ' 'resource policies.') logger.debug('Looking for cached policy for {}'.format(arn)) # manipulate the ARN as needed service = arns.get_service(arn) if service == 's3': # we only need the ARN of the bucket search_arn = 'arn:{}:s3:::{}'.format(arns.get_partition(arn), arns.get_resource(arn).split('/')[0]) elif service == 'iam': # special case: trust policies role_name = arns.get_resource(arn).split('/')[-1] # get the last part of :role/path/to/role_name role_node = graph.get_node_by_searchable_name('role/{}'.format(role_name)) return role_node.trust_policy elif service == 'sns': search_arn = arn elif service == 'sqs': search_arn = arn elif service == 'kms': search_arn = arn elif service == 'secretsmanager': search_arn = arn else: raise NotImplementedError('Service policies for {} are not (currently) cached.'.format(service)) for policy in graph.policies: if search_arn == policy.arn: return policy raise ValueError('Unable to locate a cached policy for resource {}'.format(arn))
def pull_cached_resource_policy_by_arn(policies: List[Policy], arn: Optional[str], query: str = None) -> Policy: """Function that pulls a resource policy that's cached on-disk. Raises ValueError if it is not able to be retrieved. Returns the dict, not the Policy object. """ if query is not None: if arn is not None: raise ValueError('Must specify either arn or query, not both.') pattern = re.compile(r'.*(arn:[^:]*:[^:]*:[^:]*:[^:]*:\S+).*') matches = pattern.match(query) if matches is None: raise ValueError('Resource policy retrieval error: could not extract resource ARN from query') arn = matches.group(1) if '?' in arn or '*' in arn: raise ValueError('Resource component from query must not have wildcard (? or *) when evaluating ' 'resource policies.') # manipulate the ARN as needed service = arns.get_service(arn) if service == 's3': # we only need the ARN of the bucket search_arn = 'arn:{}:s3:::{}'.format(arns.get_partition(arn), arns.get_resource(arn).split('/')[0]) elif service == 'iam': search_arn = arn elif service == 'sns': search_arn = arn elif service == 'sqs': search_arn = arn elif service == 'kms': search_arn = arn elif service == 'secretsmanager': search_arn = arn else: raise NotImplementedError('Service policies for {} are not (currently) cached.'.format(service)) for policy in policies: if search_arn == policy.arn: return policy raise ValueError('Unable to locate a cached policy for resource {}'.format(arn))
def local_check_authorization_full( principal: Node, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, resource_policy: Optional[dict] = None, resource_owner: Optional[str] = None, service_control_policy_groups: Optional[List[List[Policy]]] = None, session_policy: Optional[dict] = None) -> bool: """Determine if a given node is authorized to make an API call. It will perform a full local policy evaluation, which includes: * Checking for any matching Deny statements in all policies that are given * Checking Organization SCPs (if given) * Checking the resource policy (if given) * Checking the principal's permission boundaries (if the caller has any attached) * Checking the session policy (if given) * Checking the principal's policies This will add condition keys that may be inferred, assuming they are not already set, such as the aws:username or aws:userid keys. If the resource_policy param is not None but the resource_owner is None, this raises a ValueError, so that must be sorted beforehand by any code calling this function.""" if resource_policy is not None and resource_owner is None: raise ValueError( 'Must specify the AWS Account ID of the owner of the resource when specifying a resource policy' ) conditions_keys_copy = copy.deepcopy(condition_keys_to_check) conditions_keys_copy.update( _infer_condition_keys(principal, conditions_keys_copy)) logger.debug( 'Testing authorization for: principal: {}, action: {}, resource: {}, conditions: {}, Resource Policy: {}, SCPs: {}, Session Policy: {}' .format(principal.arn, action_to_check, resource_to_check, conditions_keys_copy, resource_policy, service_control_policy_groups, session_policy)) # Check all policies for a matching deny for policy in principal.attached_policies: if policy_has_matching_statement(policy, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explicit Deny: Principal\'s attached policies.') return False if service_control_policy_groups is not None: for service_control_policy_group in service_control_policy_groups: for service_control_policy in service_control_policy_group: if policy_has_matching_statement(service_control_policy, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explicit Deny: SCPs') return False if resource_policy is not None: rp_matching_statements = resource_policy_matching_statements( principal, resource_policy, action_to_check, resource_to_check, conditions_keys_copy) for statement in rp_matching_statements: if statement['Effect'] == 'Deny': logger.debug('Explicit Deny: Resource Policy') return False if session_policy is not None: if policy_has_matching_statement(session_policy, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explict Deny: Session policy') return False if principal.permissions_boundary is not None: if policy_has_matching_statement(principal.permissions_boundary, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explicit Deny: Permission Boundary') return False # Check SCPs if service_control_policy_groups is not None: for service_control_policy_group in service_control_policy_groups: # For every group of SCPs (policies attached to the ancestors of the account and the current account), the # group of SCPs have to have a matching allow statement scp_group_result = False for service_control_policy in service_control_policy_group: if policy_has_matching_statement(service_control_policy, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): scp_group_result = True break if not scp_group_result: logger.debug('Implicit Deny: SCP group') return False # Check resource policy if resource_policy is not None: rp_auth_result = resource_policy_authorization( principal, resource_owner, resource_policy, action_to_check, resource_to_check, conditions_keys_copy) if arns.get_account_id(principal.arn) == resource_owner: # resource is owned by account if arns.get_service(resource_to_check) in ( 'iam', 'kms'): # TODO: tuple or list? # IAM and KMS require the trust/key policy to match if rp_auth_result is not ResourcePolicyEvalResult.NODE_MATCH and rp_auth_result is not ResourcePolicyEvalResult.ROOT_MATCH: logger.debug( 'IAM/KMS Denial: RP must authorize even with same account' ) return False if rp_auth_result is ResourcePolicyEvalResult.NODE_MATCH: # If the specific IAM User/Role is given in the resource policy's Principal element and from the same # account as the resource, we're done since we've already done deny-checks and the permission boundaries # + session policy + principal policies aren't necessary to grant authorization logger.debug('RP approval: skip further evaluation') return True else: # resource is owned by another account if rp_auth_result is ResourcePolicyEvalResult.NO_MATCH: logger.debug('Cross-Account authorization denied') return False # Check permission boundary if principal.permissions_boundary is not None: if not policy_has_matching_statement( principal.permissions_boundary, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Implicit Deny: Permission Boundary') return False # Check session policy if session_policy is not None: if not policy_has_matching_statement( session_policy, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Implicit Deny: Session Policy') return False # Check principal's policies for policy in principal.attached_policies: if policy_has_matching_statement(policy, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): return True # already did Deny statement checks, so we're done logger.debug('Implicit Deny: Principal\'s Attached Policies') return False
def process_arguments(parsed_args: Namespace): """Given a namespace object generated from parsing args, perform the appropriate tasks. Returns an int matching expectations set by /usr/include/sysexits.h for command-line utilities.""" if parsed_args.account is None: session = botocore_tools.get_session(parsed_args.profile) else: session = None graph = graph_actions.get_existing_graph(session, parsed_args.account) logger.debug('Querying against graph {}'.format( graph.metadata['account_id'])) # process condition args to generate input dict conditions = {} if parsed_args.condition is not None: for arg in parsed_args.condition: # split on equals-sign (=), assume first instance separates the key and value components = arg.split('=') if len(components) < 2: print('Format for condition args not matched: <key>=<value>') return 64 key = components[0] value = '='.join(components[1:]) conditions.update({key: value}) if parsed_args.with_resource_policy: resource_policy = query_utils.pull_cached_resource_policy_by_arn( graph, parsed_args.resource) elif parsed_args.resource_policy_text: resource_policy = json.loads(parsed_args.resource_policy_text) else: resource_policy = None resource_owner = parsed_args.resource_owner if resource_policy is not None: if parsed_args.resource_owner is None: if arns.get_service(resource_policy.arn) == 's3': raise ValueError( 'Must supply resource owner (--resource-owner) when including S3 bucket policies ' 'in a query') else: resource_owner = arns.get_account_id(resource_policy.arn) if isinstance(resource_policy, Policy): resource_policy = resource_policy.policy_doc if parsed_args.scps: if 'org-id' in graph.metadata and 'org-path' in graph.metadata: org_tree_path = os.path.join(get_storage_root(), graph.metadata['org-id']) org_tree = OrganizationTree.create_from_dir(org_tree_path) scps = query_orgs.produce_scp_list(graph, org_tree) else: raise ValueError( 'Graph for account {} does not have an associated OrganizationTree mapped (need to run ' '`pmapper orgs create/update` to get that.') else: scps = None query_actions.argquery(graph, parsed_args.principal, parsed_args.action, parsed_args.resource, conditions, parsed_args.preset, parsed_args.skip_admin, resource_policy, resource_owner, parsed_args.include_unauthorized, parsed_args.session_policy, scps) return 0
def gen_resources_with_potential_confused_deputies(graph: Graph) -> List[Finding]: """Generates findings related to AWS resources that allow access to AWS services (via resource policy) that may not correctly verify which AWS account is the true source of a request that affects the given resource. Primarily works by inspecting resource policies and making sure that access is guarded with a condition using aws:SourceAccount.""" result = [] resource_service_action_map = { 's3': { 'serverlessrepo.amazonaws.com': [ 's3:GetObject' ] } } affected_policies = [] # type: List[Tuple[str, str, str]] for resource_type in resource_service_action_map.keys(): for policy in graph.policies: if arns.get_service(policy.arn) == resource_type: for service, action_list in resource_service_action_map[resource_type].items(): available_actions = [] for action in action_list: rpa_result = resource_policy_authorization( service, graph.metadata['account_id'], policy.policy_doc, action, policy.arn, { 'aws:SourceAccount': '000000000000' } ) if rpa_result.SERVICE_MATCH: available_actions.append(action) if len(available_actions) > 0: affected_policies.append( (policy.arn, service, ' | '.join(available_actions)) ) if len(affected_policies) > 0: desc_list_str = '\n'.join(['* With service {}, the resource {} for the action(s): {}'.format(y, x, z) for x, y, z in affected_policies]) result.append( Finding( 'Resources With A Potential Confused-Deputy Risk', 'Medium', 'Depending on the affected resources and services, an attacker may be able to execute read or write ' 'operations on the resources from another AWS account.', 'In AWS, certain services will create and use resources in the customer\'s own AWS account. This may ' 'be controlled using a resource policy that grants access to the service that created the resource ' 'in the customer\'s AWS account. However, some services require customers to use the ' '`${aws:SourceAccount}` condition context key to control access to the account resource from the ' 'service. In other words, to prevent the service from accessing the resource on the behalf of ' 'another customer, the resource needs a resource policy that allow-lists the true "source" of a ' 'request.\n\n' 'The following AWS services and resources could allow an external account to potentially gain ' 'read/write access to the resources:\n\n' + desc_list_str, 'Update the resource policy for all affected resources, and ensure that all statements granting ' 'access to AWS services check against the `${aws:SourceAccount}` condition context key when ' 'appropriate.' ) ) return result