Example #1
0
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
Example #2
0
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
Example #3
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))
Example #4
0
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))
Example #5
0
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
Example #6
0
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
Example #7
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