Example #1
0
def get_interaccount_search_list(all_graphs: List[Graph], inter_account_edges: List[Edge], node: Node) -> List[List[Edge]]:
    """Returns a list of edge lists. Each edge list represents a path to a new unique node that's accessible from the
    initial node (passed as a param). This is a breadth-first search, and the returned list of lists of edges will
    represent the different available paths.
    """

    result = []
    nodes_found = [node]
    nodes_explored = []

    account_id_graph_map = {}
    for graph in all_graphs:
        account_id_graph_map[graph.metadata['account_id']] = graph

    # Get initial list of edges
    first_set = get_edges_interaccount(account_id_graph_map[arns.get_account_id(node.arn)], inter_account_edges, node, nodes_found)
    for found_edge in first_set:
        nodes_found.append(found_edge.destination)
        result.append([found_edge])
    nodes_explored.append(node)

    # dig through result list
    index = 0
    while index < len(result):
        current_node = result[index][-1].destination
        if current_node not in nodes_explored:
            for edge in get_edges_interaccount(account_id_graph_map[arns.get_account_id(current_node.arn)], inter_account_edges, current_node, nodes_found):
                result.append(result[index][:] + [edge])
                if edge.destination not in nodes_found:
                    nodes_found.append(edge.destination)
            nodes_explored.append(current_node)
        index += 1

    return result
def _principal_matches_in_statement(principal: Node, aws_principal_field: list):
    """Helper function for locally determining a principal matches a resource policy's statement"""
    for value in aws_principal_field:
        if principal.arn == value:
            return True
        elif principal.id_value == value:
            return True
        elif arns.get_account_id(principal.arn) == value:
            return True
        else:
            principal_root_str = 'arn:{}:iam::{}:root'.format(arns.get_partition(principal.arn),
                                                              arns.get_account_id(principal.arn))
            if principal_root_str == value:
                return True
    return False
Example #3
0
def gen_overprivileged_function_findings(graph: Graph) -> List[Finding]:
    """Generates findings related to risk from Lambda functions being loaded with overprivileged roles"""
    result = []
    affected_roles = []
    for node in graph.nodes:
        if ':role/' in node.arn and node.is_admin:
            if query_interface.resource_policy_authorization('lambda.amazonaws.com', arns.get_account_id(node.arn),
                                                             node.trust_policy, 'sts:AssumeRole', node.arn, {})\
                    == query_interface.ResourcePolicyEvalResult.SERVICE_MATCH:
                affected_roles.append(node)

    if len(affected_roles) > 0:
        description_preamble = 'In AWS, Lambda functions can be assigned an IAM Role to use during execution. These ' \
                               'IAM Roles give the function access to call the AWS API with the permissions of the ' \
                               'IAM Role, depending on the policies attached to it. If the Lambda function can be ' \
                               'compromised, and the attacker can alter the code it executes, the attacker could ' \
                               'make AWS API calls with the IAM Role\'s permissions. The following IAM Roles have ' \
                               'administrative privileges, and can be passed to Lambda functions:\n\n'

        description_body = ''
        for node in affected_roles:
            description_body += '* {}\n'.format(node.searchable_name())

        result.append(Finding(
            'IAM Roles Available to Lambda Functions Have Administrative Privileges' if len(affected_roles) > 1 else
            'IAM Role Available to Lambda Functions Has Administrative Privileges',
            'Medium',
            'If an attacker can inject code or commands into the function, or if a lower-privileged principal can '
            'alter the function, the AWS account as a whole could be compromised.',
            description_preamble + description_body,
            'Reduce the scope of permissions attached to the noted IAM Role(s).'
        ))

    return result
Example #4
0
def search_authorization_across_accounts(graph_scp_pairs: List[Tuple[Graph, Optional[List[List[Policy]]]]],
                                         inter_account_edges: List[Edge], principal: Node,
                                         action_to_check: str, resource_to_check: str,
                                         condition_keys_to_check: _UODict, resource_policy: Optional[dict] = None,
                                         resource_owner: Optional[str] = None,
                                         session_policy: Optional[dict] = None) -> QueryResult:
    """Determines if the passed principal, or any principals it can access, can perform a given action for a
    given resource/condition. Handles an optional resource policy, an optional SCP list, and an optional
    session policy. The session policy is discarded after checking if the passed principal has access
    (we assume it is discarded after each pivot, and that it does NOT affect the accessibility of the edges).

    In `local_check_authorization` we usually throw up our hands if the given principal is an admin. But, because of
    how SCPs work (even blocking the root user), we force the full search to give more accurate results. If the
    SCPs param is None, we assume no SCPs are in place and can make the same assumption as in
    `local_check_authorization`.

    If the resource_owner param is not None, and the resource_owner param is None, the `local_check_authorization_full`
    function that gets called will throw a ValueError, so make sure the resource ownership is sorted before calling
    this method.

    The graphs to include in the search have to be passed in tuples. The second element of the tuple is either the SCPs
    that affect that graph or None. If your graph belongs to an organization, remember that you can take the
    OrganizationTree object and produce the applicable SCPs by calling
    principalmapper.querying.query_orgs.produce_scp_list and passing the graph + org-tree objects."""

    account_id_graph_scp_pair_map = {}
    for graph_scp_pair in graph_scp_pairs:
        account_id_graph_scp_pair_map[graph_scp_pair[0].metadata['account_id']] = graph_scp_pair
    source_graph_scp_pair = account_id_graph_scp_pair_map[arns.get_account_id(principal.arn)]

    if local_check_authorization_full(principal, action_to_check, resource_to_check, condition_keys_to_check,
                                      resource_policy, resource_owner, source_graph_scp_pair[1], session_policy):
        return QueryResult(True, [], principal)

    # now we have to check cross-account scenario for admin short-circuit
    if source_graph_scp_pair[1] is None and principal.is_admin and resource_owner == arns.get_account_id(principal.arn):
        return QueryResult(True, principal, principal)

    for edge_list in query_utils.get_interaccount_search_list([x[0] for x in graph_scp_pairs], inter_account_edges, principal):
        proxy_principal = edge_list[-1].destination
        proxy_principal_scps = account_id_graph_scp_pair_map[arns.get_account_id(proxy_principal.arn)][1]
        if local_check_authorization_full(edge_list[-1].destination, action_to_check, resource_to_check, condition_keys_to_check,
                                          resource_policy, resource_owner, proxy_principal_scps, None):
            return QueryResult(True, edge_list, principal)

    return QueryResult(False, [], principal)
    def _check_assume_role(ga, na, gb, nb, scps) -> bool:
        logger.debug('Checking if {} can access {}'.format(na.arn, nb.arn))

        # load up conditions: inspired by _infer_condition_keys
        conditions = {}
        conditions['aws:CurrentTime'] = dt.datetime.now(
            dt.timezone.utc).isoformat()
        conditions['aws:EpochTime'] = str(
            round(dt.datetime.now(dt.timezone.utc).timestamp()))
        conditions['aws:userid'] = na.id_value

        if ':user/' in na.arn:
            conditions['aws:username'] = na.searchable_name().split('/')[1]

        conditions['aws:SecureTransport'] = 'true'
        conditions['aws:PrincipalAccount'] = ga.metadata['account_id']
        conditions['aws:PrincipalArn'] = na.arn
        if 'org-id' in ga.metadata:
            conditions['aws:PrincipalOrgID'] = ga.metadata['org-id']
        if 'org-path' in ga.metadata:
            conditions['aws:PrincipalOrgPaths'] = ga.metadata['org-path']

        for tag_key, tag_value in na.tags.items():
            conditions['aws:PrincipalTag/{}'.format(tag_key)] = tag_value

        # check without MFA
        auth_result = local_check_authorization_full(
            na, 'sts:AssumeRole', nb.arn, conditions, nb.trust_policy,
            arns.get_account_id(nb.arn), scps)

        if auth_result:
            return True

        # check with MFA
        conditions.update({
            'aws:MultiFactorAuthAge': '1',
            'aws:MultiFactorAuthPresent': 'true'
        })
        auth_result = local_check_authorization_full(
            na, 'sts:AssumeRole', nb.arn, conditions, nb.trust_policy,
            arns.get_account_id(nb.arn), scps)

        return auth_result
Example #6
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 #7
0
def resource_policy_authorization(
    node_or_service: Union[Node, str], resource_owner: str,
    resource_policy: dict, action_to_check: str, resource_to_check: str,
    condition_keys_to_check: Union[dict, CaseInsensitiveDict]
) -> ResourcePolicyEvalResult:
    """Returns a ResourcePolicyEvalResult for a given request, based on the resource policy."""

    if isinstance(condition_keys_to_check, dict):
        prepped_condition_keys = CaseInsensitiveDict(condition_keys_to_check)
    else:
        prepped_condition_keys = condition_keys_to_check

    matching_statements = resource_policy_matching_statements(
        node_or_service, resource_policy, action_to_check, resource_to_check,
        prepped_condition_keys)
    if len(matching_statements) == 0:
        return ResourcePolicyEvalResult.NO_MATCH

    # handle denies outright
    for statement in matching_statements:
        if statement['Effect'] == 'Deny':
            return ResourcePolicyEvalResult.DENY_MATCH

    # handle nodes (IAM Users or Roles)
    if isinstance(node_or_service, Node):
        # if in a different account, check for denies and wrap it up
        if arns.get_account_id(node_or_service.arn) != resource_owner:
            return ResourcePolicyEvalResult.DIFF_ACCOUNT_MATCH

        else:
            node_match = False
            for statement in matching_statements:
                if 'NotPrincipal' in statement:
                    # NotPrincipal means a node match (tested with S3)
                    node_match = True
                elif isinstance(statement['Principal'],
                                str) and statement['Principal'] == '*':
                    # Case: "Principal": "*"
                    node_match = True
                else:
                    # dig through 'AWS' element of Principal for node-matching
                    if 'AWS' in statement['Principal']:
                        for aws_principal in _listify_string(
                                statement['Principal']['AWS']):
                            if node_or_service.arn == aws_principal:
                                node_match = True
            if node_match:
                return ResourcePolicyEvalResult.NODE_MATCH
            else:
                return ResourcePolicyEvalResult.ROOT_MATCH

    else:
        return ResourcePolicyEvalResult.SERVICE_MATCH
Example #8
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
def resource_policy_authorization(node_or_service: Union[Node, str], resource_owner: str, resource_policy: dict,
                                  action_to_check: str, resource_to_check: str, condition_keys_to_check: dict,
                                  debug: bool) -> ResourcePolicyEvalResult:
    """Returns a ResourcePolicyEvalResult for a given request, based on the resource policy."""
    dprint(debug, "Local resource policy authorization check: Principal {}, Action {}, Resource {}, Condition Keys {}, "
                  "Resource Owner {}".format(node_or_service, action_to_check, resource_to_check,
                                             condition_keys_to_check, resource_owner))

    matching_statements = resource_policy_matching_statements(node_or_service, resource_policy, action_to_check,
                                                              resource_to_check, condition_keys_to_check, debug)
    if len(matching_statements) == 0:
        return ResourcePolicyEvalResult.NO_MATCH

    # handle nodes (IAM Users or Roles)
    if isinstance(node_or_service, Node):
        # if in a different account, check for denies and wrap it up
        if arns.get_account_id(node_or_service.arn) != resource_owner:
            for statement in matching_statements:
                if statement['Effect'] == 'Deny':
                    return ResourcePolicyEvalResult.DENY_MATCH
            return ResourcePolicyEvalResult.DIFF_ACCOUNT_MATCH

        else:
            # messy part: find denies, then determine if we send back ROOT or NODE match
            for statement in matching_statements:
                if statement['Effect'] == 'Deny':
                    return ResourcePolicyEvalResult.DENY_MATCH

            node_match = False
            for statement in matching_statements:
                if 'NotPrincipal' in statement:
                    # NotPrincipal means a node match (tested with S3)
                    node_match = True
                else:
                    for principal in _listify_string(statement['Principal']):
                        if node_or_service.arn == principal:
                            node_match = True
                        if node_or_service.id_value == principal:
                            node_match = True  # 'AIDA.*' and co. can match here

            if node_match:
                return ResourcePolicyEvalResult.NODE_MATCH
            else:
                return ResourcePolicyEvalResult.ROOT_MATCH

    else:
        return ResourcePolicyEvalResult.SERVICE_MATCH
Example #10
0
def _infer_condition_keys(principal: Node, current_keys: CaseInsensitiveDict) -> CaseInsensitiveDict:
    """Returns a dictionary with global condition context keys we can infer are set based on the input Node being
    checked. We exclude setting keys that are already set in current_keys.

    Using information from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html

    Changes in v1.1.3:
        * Changed param current_keys type to CaseInsensitiveDict
        * Changed return type to CaseInsensitiveDict
    """

    result = CaseInsensitiveDict()

    # Date and Time: aws:CurrentTime and aws:EpochTime
    # TODO: Examine if using datetime.isoformat() is good enough to avoid bugs
    if 'aws:CurrentTime' not in current_keys:
        result['aws:CurrentTime'] = dt.datetime.now(dt.timezone.utc).isoformat()

    if 'aws:EpochTime' not in current_keys:
        result['aws:EpochTime'] = str(round(dt.datetime.now(dt.timezone.utc).timestamp()))

    # UserID and Username: aws:userid and aws:username
    # TODO: Double-check how roles handle aws:username, IIRC it's not filled in
    if 'aws:userid' not in current_keys:
        result['aws:userid'] = principal.id_value

    if ':user/' in principal.arn and 'aws:username' not in current_keys:
        result['aws:username'] = principal.searchable_name().split('/')[1]

    # assumes API requests are made via secure channel (HTTPS)
    if 'aws:SecureTransport' not in current_keys:
        result['aws:SecureTransport'] = 'true'

    if 'aws:PrincipalAccount' not in current_keys:
        result['aws:PrincipalAccount'] = arns.get_account_id(principal.arn)

    if 'aws:PrincipalArn' not in current_keys:
        result['aws:PrincipalArn'] = principal.arn

    # NOTE: tag keys are checked for case-insensitive equality already, no worries about collisions
    for tag_key, tag_value in principal.tags.items():
        if 'aws:PrincipalTag/{}'.format(tag_key) not in current_keys:
            result['aws:PrincipalTag/{}'.format(tag_key)] = tag_value

    return result
Example #11
0
def _infer_condition_keys(principal: Node, current_keys: dict) -> dict:
    """Returns a dictionary with global condition context keys we can infer are set based on the input Node being
    checked. We exclude setting keys that are already set in current_keys.

    Using information from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
    """

    result = {}

    # Date and Time: aws:CurrentTime and aws:EpochTime
    # TODO: Examine if using datetime.isoformat() is good enough to avoid bugs
    if 'aws:CurrentTime' not in current_keys:
        result['aws:CurrentTime'] = dt.datetime.now(
            dt.timezone.utc).isoformat()

    if 'aws:EpochTime' not in current_keys:
        result['aws:EpochTime'] = str(
            round(dt.datetime.now(dt.timezone.utc).timestamp()))

    # UserID and Username: aws:userid and aws:username
    # TODO: Double-check how roles handle aws:username, IIRC it's not filled in
    if 'aws:userid' not in current_keys:
        result['aws:userid'] = principal.id_value

    if ':user/' in principal.arn and 'aws:username' not in current_keys:
        result['aws:username'] = principal.searchable_name().split('/')[1]

    if 'aws:SecureTransport' not in current_keys:
        result['aws:SecureTransport'] = 'true'

    if 'aws:PrincipalAccount' not in current_keys:
        result['aws:PrincipalAccount'] = arns.get_account_id(principal.arn)

    if 'aws:PrincipalArn' not in current_keys:
        result['aws:PrincipalArn'] = principal.arn

    for tag_key, tag_value in principal.tags.items():
        if 'aws:PrincipalTag/{}'.format(tag_key) not in current_keys:
            result['aws:PrincipalTag/{}'.format(tag_key)] = tag_value

    return result
Example #12
0
def gen_overprivileged_stack_findings(graph: Graph) -> List[Finding]:
    """Generates findings related to risk from CloudFormation stacks being loaded with overprivileged roles"""
    result = []
    affected_roles = []
    for node in graph.nodes:
        if ':role/' in node.arn and node.is_admin:
            if query_interface.resource_policy_authorization('cloudformation.amazonaws.com',
                                                             arns.get_account_id(node.arn), node.trust_policy,
                                                             'sts:AssumeRole', node.arn, {}) == \
                    query_interface.ResourcePolicyEvalResult.SERVICE_MATCH:
                affected_roles.append(node)

    if len(affected_roles) > 0:
        description_preamble = 'In AWS, CloudFormation stacks can be given an IAM Role. When a stack has an IAM ' \
                               'Role, it can use that IAM Role to make AWS API calls to create the resources ' \
                               'defined in the template for that stack. If the IAM Role has administrator access ' \
                               'to the account, and an attacker is able to make the right CloudFormation API calls, ' \
                               'they would be able to use the IAM Role to escalate privileges and compromise the ' \
                               'account as a whole. The following IAM Roles can be used in CloudFormation and ' \
                               'have administrative privileges:\n\n'

        description_body = ''
        for node in affected_roles:
            description_body += '* {}\n'.format(node.searchable_name())

        result.append(
            Finding(
                'IAM Roles Available to CloudFormation Stacks Have Administrative Privileges'
                if len(affected_roles) > 1 else
                'IAM Role Available to CloudFormation Stacks Has Administrative Privileges',
                'Low',
                'If an attacker has the right permissions in the AWS Account, they can grant themselves adminstrative '
                'access to the account to compromise the account.',
                description_preamble + description_body,
                'Reduce the scope of permissions attached to the noted IAM Role(s).'
            ))

    return result
Example #13
0
def generate_edges_locally(
        nodes: List[Node],
        scps: Optional[List[List[dict]]] = None,
        codebuild_projects: Optional[List[dict]] = None) -> List[Edge]:
    """Generates and returns Edge objects related to AWS CodeBuild.

    It is possible to use this method if you are operating offline (infra-as-code). The `codebuild_projects` param
    should be a list of dictionary objects with the following expected structure:

    ```
    {
        'project_arn': <str: ARN of a project>,
        'project_role': <str: ARN of a role attached to a project>
        'project_tags': <list[dict]: tags for the project as in [{'Key': <Key>, 'Value': <Value>}]>
    }
    ```

    All elements are required, tags must point to an empty list if there are no tags attached to the project
    """

    result = []

    # we wanna create a role -> [{proj_arn: <>, proj_tags: <>}] map to make eventual lookups faster
    if codebuild_projects is None:
        codebuild_map = {}
    else:
        codebuild_map = {}  # type: Dict[str, List[dict]]
        for project in codebuild_projects:
            if project['project_role'] not in codebuild_map:
                codebuild_map[project['project_role']] = [{
                    'proj_arn':
                    project['project_arn'],
                    'proj_tags':
                    project['project_tags']
                }]
            else:
                codebuild_map[project['project_role']].append({
                    'proj_arn':
                    project['project_arn'],
                    'proj_tags':
                    project['project_tags']
                })

    for node_destination in nodes:
        # check if destination is a user, skip if so
        if ':role/' not in node_destination.arn:
            continue

        # check that the destination role can be assumed by CodeBuild
        sim_result = resource_policy_authorization(
            'codebuild.amazonaws.com',
            arns.get_account_id(node_destination.arn),
            node_destination.trust_policy,
            'sts:AssumeRole',
            node_destination.arn,
            {},
        )

        if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
            continue  # CodeBuild wasn't auth'd to assume the role

        for node_source in nodes:
            # skip self-access checks
            if node_source == node_destination:
                continue

            # check if source is an admin: if so, it can access destination but this is not tracked via an Edge
            if node_source.is_admin:
                continue

            # check if source can use existing projects
            if node_destination.arn in codebuild_map:
                projects = codebuild_map[node_destination.arn]
                for project in projects:
                    startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'codebuild:StartBuild',
                        project['proj_arn'],
                        _gen_resource_tag_conditions(project['proj_tags']),
                        service_control_policy_groups=scps)
                    if startproj_auth:
                        result.append(
                            Edge(
                                node_source, node_destination,
                                '(MFA Required) can use CodeBuild with an existing project to access'
                                if startproj_mfa else
                                'can use CodeBuild with an existing project to access',
                                'CodeBuild'))
                        break  # break out of iterating through projects

                    batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'codebuild:StartBuildBatch',
                        project['proj_arn'],
                        _gen_resource_tag_conditions(project['proj_tags']),
                        service_control_policy_groups=scps)
                    if batchstartproj_auth:
                        result.append(
                            Edge(
                                node_source, node_destination,
                                '(MFA Required) can use CodeBuild with an existing project to access'
                                if startproj_mfa else
                                'can use CodeBuild with an existing project to access',
                                'CodeBuild'))
                        break  # break out of iterating through projects

            # check if source can create/update a project, pass this role, then start a build
            condition_keys = {'iam:PassedToService': 'codebuild.amazonaws.com'}
            pass_role_auth, pass_role_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'iam:PassRole',
                node_destination.arn,
                condition_keys,
                service_control_policy_groups=scps)

            if not pass_role_auth:
                continue  # if we can't pass this role, then we're done

            # check if the source can create a project and start a build
            create_proj_auth, create_proj_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'codebuild:CreateProject',
                '*', {},
                service_control_policy_groups=scps)
            if create_proj_auth:
                startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'codebuild:StartBuild',
                    '*', {},
                    service_control_policy_groups=scps)
                if startproj_auth:
                    result.append(
                        Edge(
                            node_source, node_destination,
                            '(MFA Required) can create a project in CodeBuild to access'
                            if create_proj_mfa or pass_role_mfa else
                            'can create a project in CodeBuild to access',
                            'CodeBuild'))
                else:
                    batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'codebuild:StartBuildBatch',
                        '*', {},
                        service_control_policy_groups=scps)
                    if batchstartproj_auth:
                        result.append(
                            Edge(
                                node_source, node_destination,
                                '(MFA Required) can create a project in CodeBuild to access'
                                if create_proj_mfa or pass_role_mfa else
                                'can create a project in CodeBuild to access',
                                'CodeBuild'))

            # check if the source can update a project and start a build
            for project in codebuild_projects:
                update_proj_auth, update_proj_mfa = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'codebuild:UpdateProject',
                    project['project_arn'],
                    _gen_resource_tag_conditions(project['project_tags']),
                    service_control_policy_groups=scps)
                if update_proj_auth:
                    startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'codebuild:StartBuild',
                        project['project_arn'],
                        _gen_resource_tag_conditions(project['project_tags']),
                        service_control_policy_groups=scps)
                    if startproj_auth:
                        result.append(
                            Edge(
                                node_source, node_destination,
                                '(MFA Required) can update a project in CodeBuild to access'
                                if create_proj_mfa or pass_role_mfa else
                                'can update a project in CodeBuild to access',
                                'CodeBuild'))
                        break  # just wanna find that there exists one updatable/usable project
                    else:
                        batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa(
                            node_source,
                            'codebuild:StartBuildBatch',
                            project['project_arn'],
                            _gen_resource_tag_conditions(
                                project['project_tags']),
                            service_control_policy_groups=scps)
                        if batchstartproj_auth:
                            result.append(
                                Edge(
                                    node_source, node_destination,
                                    '(MFA Required) can update a project in CodeBuild to access'
                                    if create_proj_mfa or pass_role_mfa else
                                    'can update a project in CodeBuild to access',
                                    'CodeBuild'))
                            break  # just wanna find that there exists one updatable/usable project

    return result
def generate_edges_locally(nodes: List[Node], stack_list: List[dict], scps: Optional[List[List[dict]]] = None) -> List[Edge]:
    """Generates and returns Edge objects. Works on the assumption that the param `stack_list` is the
    collected outputs from calling `cloudformation:DescribeStacks`. Thus, it is possible to
    create a similar output and feed it to this method if you are operating offline (infra-as-code).
    """

    result = []

    for node_destination in nodes:
        # check if the destination is a role
        if ':role/' not in node_destination.arn:
            continue

        # check that the destination role can be assumed by CloudFormation
        sim_result = resource_policy_authorization(
            'cloudformation.amazonaws.com',
            arns.get_account_id(node_destination.arn),
            node_destination.trust_policy,
            'sts:AssumeRole',
            node_destination.arn,
            {}
        )

        if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
            continue  # CloudFormation wasn't auth'd to assume the role

        for node_source in nodes:
            # skip self-access checks
            if node_source == node_destination:
                continue

            # check if source is an admin: if so, it can access destination but this is not tracked via an Edge
            if node_source.is_admin:
                continue

            # Get iam:PassRole info
            can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'iam:PassRole',
                node_destination.arn,
                {
                    'iam:PassedToService': 'cloudformation.amazonaws.com'
                },
                service_control_policy_groups=scps
            )

            # See if source can make a new stack and pass the destination role
            if can_pass_role:
                can_create, need_mfa_create = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'cloudformation:CreateStack',
                    '*',
                    {'cloudformation:RoleArn': node_destination.arn},
                    service_control_policy_groups=scps
                )
                if can_create:
                    reason = 'can create a stack in CloudFormation to access'
                    if need_mfa_passrole or need_mfa_create:
                        reason = '(MFA required) ' + reason

                    result.append(Edge(node_source, node_destination, reason, 'Cloudformation'))

            relevant_stacks = []  # we'll reuse this for *ChangeSet
            for stack in stack_list:
                if 'RoleArn' in stack:
                    if stack['RoleARN'] == node_destination.arn:
                        relevant_stacks.append(stack)

            # See if source can call UpdateStack to use the current role of a stack (setting a new template)
            for stack in relevant_stacks:
                can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'cloudformation:UpdateStack',
                    stack['StackId'],
                    {'cloudformation:RoleArn': node_destination.arn},
                    service_control_policy_groups=scps
                )
                if can_update:
                    reason = 'can update the CloudFormation stack {} to access'.format(
                        stack['StackId']
                    )
                    if need_mfa_update:
                        reason = '(MFA required) ' + reason

                    result.append(Edge(node_source, node_destination, reason, 'Cloudformation'))
                    break  # let's save ourselves having to dig into every CF stack edge possible

            # See if source can call UpdateStack to pass a new role to a stack and use it
            if can_pass_role:
                for stack in stack_list:
                    can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'cloudformation:UpdateStack',
                        stack['StackId'],
                        {'cloudformation:RoleArn': node_destination.arn},
                        service_control_policy_groups=scps
                    )

                    if can_update:
                        reason = 'can update the CloudFormation stack {} and pass the role to access'.format(
                            stack['StackId']
                        )
                        if need_mfa_update or need_mfa_passrole:
                            reason = '(MFA required) ' + reason

                        result.append(Edge(node_source, node_destination, reason, 'Cloudformation'))
                        break  # save ourselves from digging into all CF stack edges possible

            # See if source can call CreateChangeSet and ExecuteChangeSet to alter a stack with a given role
            for stack in relevant_stacks:
                can_make_cs, need_mfa_make = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'cloudformation:CreateChangeSet',
                    stack['StackId'],
                    {'cloudformation:RoleArn': node_destination.arn},
                    service_control_policy_groups=scps
                )
                if not can_make_cs:
                    continue

                can_exe_cs, need_mfa_exe = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'cloudformation:ExecuteChangeSet',
                    stack['StackId'],
                    {},  # docs say no RoleArn context here
                    service_control_policy_groups=scps
                )

                if can_exe_cs:
                    reason = 'can create and execute a changeset in CloudFormation for stack {} to access'.format(
                        stack['StackId']
                    )
                    if need_mfa_make or need_mfa_exe:
                        reason = '(MFA required) ' + reason

                    result.append(Edge(node_source, node_destination, reason, 'Cloudformation'))
                    break  # save ourselves from digging into all CF stack edges possible

    return result
Example #15
0
def generate_edges_locally(
        nodes: List[Node],
        scps: Optional[List[List[dict]]] = None) -> List[Edge]:
    """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code).
    """

    result = []
    for node_destination in nodes:
        # check if destination is a user, skip if so
        if ':role/' not in node_destination.arn:
            continue

        # check that the destination role can be assumed by EC2
        sim_result = resource_policy_authorization(
            'ec2.amazonaws.com',
            arns.get_account_id(node_destination.arn),
            node_destination.trust_policy,
            'sts:AssumeRole',
            node_destination.arn,
            {},
        )

        if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
            continue  # EC2 wasn't auth'd to assume the role

        for node_source in nodes:
            # skip self-access checks
            if node_source == node_destination:
                continue

            # check if source is an admin: if so, it can access destination but this is not tracked via an Edge
            if node_source.is_admin:
                continue

            # check if source can pass the destination role
            mfa_needed = False
            condition_keys = {'iam:PassedToService': 'ec2.amazonaws.com'}
            pass_role_auth, mfa_res = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'iam:PassRole',
                node_destination.arn,
                condition_keys,
                service_control_policy_groups=scps)
            if not pass_role_auth:
                continue  # source can't pass the role to use it

            # check if destination has an instance profile, if not: check if source can create it
            if node_destination.instance_profile is None:
                create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'iam:CreateInstanceProfile',
                    '*', {},
                    service_control_policy_groups=scps)
                if not create_ip_auth:
                    continue  # node_source can't make the instance profile
                if mfa_res:
                    mfa_needed = True

                create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'iam:AddRoleToInstanceProfile',
                    node_destination.arn, {},
                    service_control_policy_groups=scps)
                if not create_ip_auth:
                    continue  # node_source can't attach a new instance profile to node_destination
                if mfa_res:
                    mfa_needed = True

            # check if source can run an instance with the instance profile condition, add edge if so and continue
            if node_destination.instance_profile is not None and len(
                    node_destination.instance_profile) > 0:
                iprofile = node_destination.instance_profile[0]
                condition_keys = {'ec2:InstanceProfile': iprofile}
            else:
                iprofile = '*'
                condition_keys = {}

            create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'ec2:RunInstances',
                '*',
                condition_keys,
                service_control_policy_groups=scps)

            if mfa_res:
                mfa_needed = True

            if create_instance_res:
                if iprofile is not '*':
                    reason = 'can use EC2 to run an instance with an existing instance profile to access'
                else:
                    reason = 'can use EC2 to run an instance with a newly created instance profile to access'
                if mfa_needed:
                    reason = '(MFA required) ' + reason

                new_edge = Edge(node_source, node_destination, reason, 'EC2')
                result.append(new_edge)

            # check if source can run an instance without an instance profile then add the profile, add edge if so
            create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'ec2:RunInstances',
                '*', {},
                service_control_policy_groups=scps)

            if mfa_res:
                mfa_needed = True

            if create_instance_res:
                attach_ip_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'ec2:AssociateIamInstanceProfile',
                    '*',
                    condition_keys,
                    service_control_policy_groups=scps)

                if iprofile is not '*':
                    reason = 'can use EC2 to run an instance and then associate an existing instance profile to ' \
                             'access'
                else:
                    reason = 'can use EC2 to run an instance and then attach a newly created instance profile to ' \
                             'access'

                if mfa_res or mfa_needed:
                    reason = '(MFA required) ' + reason

                if attach_ip_res:
                    new_edge = Edge(node_source, node_destination, reason,
                                    'EC2')
                    result.append(new_edge)

    return result
Example #16
0
def generate_edges_locally(
        nodes: List[Node],
        scps: Optional[List[List[dict]]] = None) -> List[Edge]:
    """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code).
    """

    result = []
    for node_destination in nodes:

        if ':role/' not in node_destination.arn:
            continue  # skip if destination is a user and not a role

        sim_result = resource_policy_authorization(
            'sagemaker.amazonaws.com',
            arns.get_account_id(node_destination.arn),
            node_destination.trust_policy, 'sts:AssumeRole',
            node_destination.arn, {})

        if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
            continue  # SageMaker is not authorized to assume the role

        for node_source in nodes:
            if node_source == node_destination:
                continue  # skip self-access checks

            if node_source.is_admin:
                continue  # skip if source is already admin, not tracked via edges

            mfa_needed = False
            conditions = {'iam:PassedToService': 'sagemaker.amazonaws.com'}
            pass_role_auth, needs_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'iam:PassRole',
                node_destination.arn,
                conditions,
                service_control_policy_groups=scps)
            if not pass_role_auth:
                continue  # source node is not authorized to pass the role

            if needs_mfa:
                mfa_needed = True

            create_notebook_auth, needs_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'sagemaker:CreateNotebookInstance',
                '*', {},
                service_control_policy_groups=scps)

            if not create_notebook_auth:
                continue  # source node is not authorized to launch the sagemaker notebook

            if needs_mfa:
                mfa_needed = True

            new_edge = Edge(
                node_source, node_destination,
                '(MFA required) can use SageMaker to launch a notebook and access'
                if mfa_needed else
                'can use SageMaker to launch a notebook and access',
                'SageMaker')
            result.append(new_edge)

    return result
Example #17
0
def query_response(graph: Graph,
                   query: str,
                   skip_admins: bool = False,
                   resource_policy: Optional[dict] = None,
                   resource_owner: Optional[str] = None,
                   include_unauthorized: bool = False,
                   session_policy: Optional[dict] = None,
                   scps: Optional[List[List[dict]]] = None) -> None:
    """Interprets, executes, and outputs the results to a query."""
    result = []

    # Parse
    tokens = re.split(r'\s+', query, flags=re.UNICODE)
    logger.debug('Query tokens: {}'.format(tokens))
    if len(tokens) < 2:
        _print_query_help()
        return

    nodes = []

    # first form: "can X do Y with Z when A B C" (principal, action, resource, conditionA, etc.)
    if tokens[0] == 'can' and tokens[2] == 'do':  # can <X> do <Y>
        nodes.append(graph.get_node_by_searchable_name(tokens[1]))
        action = tokens[3]

        if len(tokens) > 5:  # can <X> do <Y> with <Z>
            if tokens[4] != 'with':
                _print_query_help()
                return
            resource = tokens[5]
        else:
            resource = '*'

        if len(tokens) > 7:  # can <X> do <Y> with <Z> when <A> and <B> and <C>
            if tokens[6] != 'when':
                _print_query_help()
                return

            # doing this funky stuff in case condition values can have spaces
            # we make the (bad, but good enough?) assumption that condition values don't have ' and ' in them
            condition_str = ' '.join(tokens[7:])
            condition_tokens = re.split(r'\s+and\s+',
                                        condition_str,
                                        flags=re.UNICODE)
            condition = {}
            for condition_token in condition_tokens:
                # split on equals-sign (=), assume first instance separates the key and value
                components = condition_token.split('=')
                if len(components) < 2:
                    raise ValueError(
                        'Format for condition args not matched: <key>=<value>')
                key = components[0]
                value = '='.join(components[1:])
                condition.update({key: value})
            logger.debug('Conditions: {}'.format(condition))
        else:
            condition = {}

    # second form: who can do X with Y when Z and A and B and C
    elif tokens[0] == 'who' and tokens[1] == 'can' and tokens[
            2] == 'do':  # who can do X
        nodes.extend(graph.nodes)
        action = tokens[3]

        if len(tokens) > 5:  # who can do X with Y
            if tokens[4] != 'with':
                _print_query_help()
                return
            resource = tokens[5]
        else:
            resource = '*'

        if len(tokens) > 7:  # who can do X with Y when A and B and C
            if tokens[6] != 'when':
                _print_query_help()
                return

            # doing this funky stuff in case condition values can have spaces
            condition_str = ' '.join(tokens[7:])
            condition_tokens = re.split(r'\s+and\s+',
                                        condition_str,
                                        flags=re.UNICODE)
            condition = {}
            for condition_token in condition_tokens:
                # split on equals-sign (=), assume first instance separates the key and value
                components = condition_token.split('=')
                if len(components) < 2:
                    raise ValueError(
                        'Format for condition args not matched: <key>=<value>')
                key = components[0]
                value = '='.join(components[1:])
                condition.update({key: value})
            logger.debug('Conditions: {}'.format(condition))
        else:
            condition = {}

    elif tokens[0] == 'preset':
        handle_preset(graph, query, skip_admins)
        return

    else:
        _print_query_help()
        return

    # pull resource owner from arg or ARN
    if resource_policy is not None:
        if resource_owner is None:
            arn_owner = arns.get_account_id(resource)
            if '*' in arn_owner or '?' in arn_owner:
                raise ValueError(
                    'Resource arg in query cannot have wildcards (? and *) unless setting '
                    '--resource-owner')
            if arn_owner == '':
                raise ValueError(
                    'Param --resource-owner must be set if resource param does not include the '
                    'account ID.')

    # Execute
    for node in nodes:
        if not skip_admins or not node.is_admin:
            result.append(
                (search_authorization_full(graph, node, action, resource,
                                           condition, resource_policy,
                                           resource_owner, scps,
                                           session_policy), action, resource))

    # Print
    for query_result, action, resource in result:
        if query_result.allowed or include_unauthorized:
            query_result.print_result(action, resource)
            print()
Example #18
0
    def return_edges(self,
                     nodes: List[Node],
                     output: io.StringIO = os.devnull,
                     debug: bool = False) -> List[Edge]:
        """Fulfills expected method return_edges."""
        result = []

        for node_source in nodes:
            for node_destination in nodes:
                # skip self-access checks
                if node_source == node_destination:
                    continue

                # check if source is an admin: if so, it can access destination but this is not tracked via an Edge
                if node_source.is_admin:
                    continue

                # check if destination is a user, skip if so
                if ':user/' in node_destination.arn:
                    continue

                # check that the destination role can be assumed by EC2
                sim_result = resource_policy_authorization(
                    'ec2.amazonaws.com', arns.get_account_id(node_source.arn),
                    node_destination.trust_policy, 'sts:AssumeRole',
                    node_destination.arn, {}, debug)

                if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
                    continue  # EC2 wasn't auth'd to assume the role

                # check if source can pass the destination role
                mfa_needed = False
                condition_keys = {'iam:PassedToService': 'ec2.amazonaws.com'}
                pass_role_auth, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source, 'iam:PassRole', node_destination.arn,
                    condition_keys, debug)
                if not pass_role_auth:
                    continue  # source can't pass the role to use it

                # check if destination has an instance profile, if not: check if source can create it
                if node_destination.instance_profile is None:
                    create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'iam:CreateInstanceProfile', '*', {},
                        debug)
                    if not create_ip_auth:
                        continue  # node_source can't make the instance profile
                    if mfa_res:
                        mfa_needed = True

                    create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'iam:AddRoleToInstanceProfile',
                        node_destination.arn, {}, debug)
                    if not create_ip_auth:
                        continue  # node_source can't attach a new instance profile to node_destination
                    if mfa_res:
                        mfa_needed = True

                # check if source can run an instance with the instance profile condition, add edge if so and continue
                if node_destination.instance_profile is not None:
                    iprofile = node_destination.instance_profile
                    condition_keys = {'ec2:InstanceProfile': iprofile}
                else:
                    iprofile = '*'
                    condition_keys = {}

                create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source, 'ec2:RunInstances', '*', condition_keys,
                    debug)

                if mfa_res:
                    mfa_needed = True

                if create_instance_res:
                    if iprofile is not '*':
                        reason = 'can use EC2 to run an instance with an existing instance profile to access'
                    else:
                        reason = 'can use EC2 to run an instance with a newly created instance profile to access'
                    if mfa_needed:
                        reason = '(MFA required) ' + reason

                    new_edge = Edge(node_source, node_destination, reason)
                    output.write('Found new edge: {}\n'.format(
                        new_edge.describe_edge()))
                    result.append(new_edge)

                # check if source can run an instance without an instance profile then add the profile, add edge if so
                create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source, 'ec2:RunInstances', '*', {}, debug)

                if mfa_res:
                    mfa_needed = True

                if create_instance_res:
                    attach_ip_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'ec2:AssociateIamInstanceProfile', '*',
                        condition_keys, debug)

                    if iprofile is not '*':
                        reason = 'can use EC2 to run an instance and then associate an existing instance profile to ' \
                                 'access'
                    else:
                        reason = 'can use EC2 to run an instance and then attach a newly created instance profile to ' \
                                 'access'

                    if mfa_res or mfa_needed:
                        reason = '(MFA required) ' + reason

                    if attach_ip_res:
                        new_edge = Edge(node_source, node_destination, reason)
                        output.write('Found new edge: {}\n'.format(
                            new_edge.describe_edge()))
                        result.append(new_edge)

        return result
Example #19
0
    def return_edges(self,
                     nodes: List[Node],
                     output: io.StringIO = os.devnull,
                     debug: bool = False) -> List[Edge]:
        """Fulfills expected method return_edges."""
        result = []

        # Grab existing stacks in each region
        cloudformation_clients = []
        if self.session is not None:
            print(
                'Searching through CloudFormation-supported regions for existing functions.'
            )
            cf_regions = self.session.get_available_regions('cloudformation')
            for region in cf_regions:
                cloudformation_clients.append(
                    self.session.create_client('cloudformation',
                                               region_name=region))

        # grab existing cloudformation stacks
        stack_list = []
        for cf_client in cloudformation_clients:
            try:
                paginator = cf_client.get_paginator('describe_stacks')
                for page in paginator.paginate():
                    for stack in page['Stacks']:
                        if stack['StackStatus'] not in [
                                'CREATE_FAILED', 'DELETE_COMPLETE',
                                'DELETE_FAILED', 'DELETE_IN_PROGRESS'
                        ]:  # ignore unusable stacks
                            stack_list.append(stack)
            except ClientError:
                output.write(
                    'Encountered an exception when listing stacks in the region {}\n'
                    .format(cf_client.meta.region_name))

        # For each node...
        for node_source in nodes:
            for node_destination in nodes:
                # skip self-access checks
                if node_source == node_destination:
                    continue

                # check if source is an admin: if so, it can access destination but this is not tracked via an Edge
                if node_source.is_admin:
                    continue

                # check if the destination is a role
                if ':role/' not in node_destination.arn:
                    continue

                # check that the destination role can be assumed by CloudFormation
                sim_result = resource_policy_authorization(
                    'cloudformation.amazonaws.com',
                    arns.get_account_id(node_source.arn),
                    node_destination.trust_policy, 'sts:AssumeRole',
                    node_destination.arn, {}, debug)

                if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
                    continue  # CloudFormation wasn't auth'd to assume the role

                # Get iam:PassRole info
                can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa(
                    node_source, 'iam:PassRole', node_destination.arn,
                    {'iam:PassedToService': 'cloudformation.amazonaws.com'},
                    debug)

                # See if source can make a new stack and pass the destination role
                if can_pass_role:
                    can_create, need_mfa_create = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'cloudformation:CreateStack', '*',
                        {'cloudformation:RoleArn': node_destination.arn},
                        debug)
                    if can_create:
                        reason = 'can create a stack in CloudFormation to access'
                        if need_mfa_passrole or need_mfa_create:
                            reason = '(MFA required) ' + reason

                        result.append(
                            Edge(node_source, node_destination, reason))

                relevant_stacks = []  # we'll reuse this for *ChangeSet
                for stack in stack_list:
                    if stack['RoleARN'] == node_destination.arn:
                        relevant_stacks.append(stack)

                # See if source can call UpdateStack to use the current role of a stack (setting a new template)
                for stack in relevant_stacks:
                    can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'cloudformation:UpdateStack',
                        stack['StackId'],
                        {'cloudformation:RoleArn': node_destination.arn},
                        debug)
                    if can_update:
                        reason = 'can update the CloudFormation stack {} to access'.format(
                            stack['StackId'])
                        if need_mfa_update:
                            reason = '(MFA required) ' + reason

                        result.append(
                            Edge(node_source, node_destination, reason))
                        break  # let's save ourselves having to dig into every CF stack edge possible

                # See if source can call UpdateStack to pass a new role to a stack and use it
                if can_pass_role:
                    for stack in stack_list:
                        can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa(
                            node_source, 'cloudformation:UpdateStack',
                            stack['StackId'],
                            {'cloudformation:RoleArn': node_destination.arn},
                            debug)

                        if can_update:
                            reason = 'can update the CloudFormation stack {} and pass the role to access'.format(
                                stack['StackId'])
                            if need_mfa_update or need_mfa_passrole:
                                reason = '(MFA required) ' + reason

                            result.append(
                                Edge(node_source, node_destination, reason))
                            break  # save ourselves from digging into all CF stack edges possible

                # See if source can call CreateChangeSet and ExecuteChangeSet to alter a stack with a given role
                for stack in relevant_stacks:
                    can_make_cs, need_mfa_make = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'cloudformation:CreateChangeSet',
                        stack['StackId'],
                        {'cloudformation:RoleArn': node_destination.arn},
                        debug)
                    if not can_make_cs:
                        continue

                    can_exe_cs, need_mfa_exe = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'cloudformation:ExecuteChangeSet',
                        stack['StackId'],
                        {},  # docs say no RoleArn context here
                        debug)

                    if can_exe_cs:
                        reason = 'can create and execute a changeset in CloudFormation for stack {} to access'.format(
                            stack['StackId'])
                        if need_mfa_make or need_mfa_exe:
                            reason = '(MFA required) ' + reason

                        result.append(
                            Edge(node_source, node_destination, reason))
                        break  # save ourselves from digging into all CF stack edges possible

        for edge in result:
            output.write("Found new edge: {}\n".format(edge.describe_edge()))
        return result
Example #20
0
 def _describe_edge(na, nb) -> str:
     """Quick method for generating strings describing edges."""
     return '{} -> {}'.format(
         '{}/{}'.format(arns.get_account_id(na.arn), na.searchable_name()),
         '{}/{}'.format(arns.get_account_id(nb.arn), nb.searchable_name()))
Example #21
0
    def return_edges(self,
                     nodes: List[Node],
                     output: io.StringIO = os.devnull,
                     debug: bool = False) -> List[Edge]:
        """Fulfills expected method return_edges. If session object is None, runs checks in offline mode."""
        result = []

        lambda_clients = []
        if self.session is not None:
            print(
                'Searching through Lambda-supported regions for existing functions.'
            )
            lambda_regions = self.session.get_available_regions('lambda')
            for region in lambda_regions:
                lambda_clients.append(
                    self.session.create_client('lambda', region_name=region))

        # grab existing lambda functions
        function_list = []
        for lambda_client in lambda_clients:
            try:
                paginator = lambda_client.get_paginator('list_functions')
                for page in paginator.paginate(
                        PaginationConfig={'PageSize': 25}):
                    for func in page['Functions']:
                        function_list.append(func)
            except ClientError:
                output.write(
                    'Encountered an exception when listing functions in the region {}\n'
                    .format(lambda_client.meta.region_name))

        for node_source in nodes:
            for node_destination in nodes:
                # skip self-access checks
                if node_source == node_destination:
                    continue

                # check if source is an admin, if so it can access destination but this is not tracked via an Edge
                if node_source.is_admin:
                    continue

                # check that destination is a role
                if ':role/' not in node_destination.arn:
                    continue

                # check that the destination role can be assumed by Lambda
                sim_result = resource_policy_authorization(
                    'lambda.amazonaws.com',
                    arns.get_account_id(node_source.arn),
                    node_destination.trust_policy, 'sts:AssumeRole',
                    node_destination.arn, {}, debug)

                if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
                    continue  # Lambda wasn't auth'd to assume the role

                # check that source can pass the destination role (store result for future reference)
                can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa(
                    node_source, 'iam:PassRole', node_destination.arn,
                    {'iam:PassedToService': 'lambda.amazonaws.com'}, debug)

                # check that source can create a Lambda function and pass it an execution role
                if can_pass_role:
                    can_create_function, need_mfa_0 = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'lambda:CreateFunction', '*', {}, debug)
                    if can_create_function:
                        if need_mfa_0 or need_mfa_passrole:
                            reason = '(requires MFA) can use Lambda to create a new function with arbitrary code, ' \
                                     'then pass and access'
                        else:
                            reason = 'can use Lambda to create a new function with arbitrary code, then pass and access'
                        new_edge = Edge(node_source, node_destination, reason)
                        output.write('Found new edge: {}\n'.format(
                            new_edge.describe_edge()))
                        result.append(new_edge)

                # List of (<function>, bool, bool, bool)
                func_data = []
                for func in function_list:
                    can_change_code, need_mfa_1 = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'lambda:UpdateFunctionCode',
                        func['FunctionArn'], {}, debug)
                    can_change_config, need_mfa_2 = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'lambda:UpdateFunctionConfiguration',
                        func['FunctionArn'], {}, debug)
                    func_data.append(
                        (func, can_change_code, can_change_config,
                         need_mfa_passrole or need_mfa_1 or need_mfa_2))

                # check that source can modify a Lambda function and use its existing role
                for func, can_change_code, can_change_config, need_mfa in func_data:
                    if node_destination.arn == func['Role']:
                        if can_change_code:
                            if need_mfa:
                                reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format(
                                    func['FunctionArn'])
                            else:
                                reason = 'can use Lambda to edit an existing function ({}) to access'.format(
                                    func['FunctionArn'])
                            new_edge = Edge(node_source, node_destination,
                                            reason)
                            output.write('Found new edge: {}\n'.format(
                                new_edge.describe_edge()))
                            break

                # check that source can modify a Lambda function and pass it another execution role
                for func, can_change_code, can_change_config, need_mfa in func_data:
                    if can_change_config and can_change_code and can_pass_role:
                        if need_mfa:
                            reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format(
                                func['FunctionArn'])
                        else:
                            reason = 'can use Lambda to edit an existing function ({}) to access'.format(
                                func['FunctionArn'])
                        new_edge = Edge(node_source, node_destination, reason)
                        output.write('Found new edge: {}\n'.format(
                            new_edge.describe_edge()))
                        break

        return result
Example #22
0
def generate_edges_locally(
        nodes: List[Node],
        function_list: List[dict],
        scps: Optional[List[List[dict]]] = None) -> List[Edge]:
    """Generates and returns Edge objects. It is possible to use this method if you are operating offline
    (infra-as-code), but you must provide a `function_list` that is a list of dictionaries that mimic the
    output of calling `lambda:ListFunctions`.
    """

    result = []
    for node_destination in nodes:
        # check that destination is a role
        if ':role/' not in node_destination.arn:
            continue

        # check that the destination role can be assumed by Lambda
        sim_result = resource_policy_authorization(
            'lambda.amazonaws.com', arns.get_account_id(node_destination.arn),
            node_destination.trust_policy, 'sts:AssumeRole',
            node_destination.arn, {})

        if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
            continue  # Lambda wasn't auth'd to assume the role

        for node_source in nodes:
            # skip self-access checks
            if node_source == node_destination:
                continue

            # check if source is an admin, if so it can access destination but this is not tracked via an Edge
            if node_source.is_admin:
                continue

            # check that source can pass the destination role (store result for future reference)
            can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'iam:PassRole',
                node_destination.arn,
                {'iam:PassedToService': 'lambda.amazonaws.com'},
                service_control_policy_groups=scps)

            # check that source can create a Lambda function and pass it an execution role
            if can_pass_role:
                can_create_function, need_mfa_0 = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'lambda:CreateFunction',
                    '*', {},
                    service_control_policy_groups=scps)
                if can_create_function:
                    if need_mfa_0 or need_mfa_passrole:
                        reason = '(requires MFA) can use Lambda to create a new function with arbitrary code, ' \
                                 'then pass and access'
                    else:
                        reason = 'can use Lambda to create a new function with arbitrary code, then pass and access'
                    new_edge = Edge(node_source, node_destination, reason,
                                    'Lambda')
                    result.append(new_edge)
                    continue  # TODO: reexamine if it is appropriate to skip the next checks, which can be O(n^2) in some accounts

            func_data = []  # List[Tuple[dict, bool, bool]]
            for func in function_list:
                can_change_code, need_mfa_1 = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'lambda:UpdateFunctionCode',
                    func['FunctionArn'], {},
                    service_control_policy_groups=scps)

                func_data.append((func, can_change_code, need_mfa_passrole
                                  or need_mfa_1))

            # check that source can modify a Lambda function and use its existing role
            for func, can_change_code, need_mfa in func_data:
                if node_destination.arn == func['Role']:
                    if can_change_code:
                        if need_mfa:
                            reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format(
                                func['FunctionArn'])
                        else:
                            reason = 'can use Lambda to edit an existing function ({}) to access'.format(
                                func['FunctionArn'])
                        new_edge = Edge(node_source, node_destination, reason,
                                        'Lambda')
                        result.append(new_edge)
                        break

                can_change_config, need_mfa_2 = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'lambda:UpdateFunctionConfiguration',
                    func['FunctionArn'], {},
                    service_control_policy_groups=scps)

                if can_change_config and can_change_code and can_pass_role:
                    if need_mfa or need_mfa_2:
                        reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format(
                            func['FunctionArn'])
                    else:
                        reason = 'can use Lambda to edit an existing function ({}) to access'.format(
                            func['FunctionArn'])
                    new_edge = Edge(node_source, node_destination, reason,
                                    'Lambda')
                    result.append(new_edge)
                    break

    return result
Example #23
0
def generate_edges_locally(
        nodes: List[Node],
        scps: Optional[List[List[dict]]] = None,
        launch_configs: Optional[List[dict]] = None) -> List[Edge]:
    """Generates and returns Edge objects related to EC2 AutoScaling.

    It is possible to use this method if you are operating offline (infra-as-code). The `launch_configs` param
    should be a list of dictionary objects with the following expected structure:

    ~~~
    {
        'lc_arn': <Launch Configurations ARN>,
        'lc_iip': <IAM Instance Profile>
    }
    ~~~

    All elements are required, but if there is no instance profile then set the field to None.
    """

    result = []

    # iterate through nodes, setting up the map as well as identifying if the service role is available
    role_lc_map = {}
    service_role_available = False
    for node in nodes:
        # this should catch the normal service role + custom ones with the suffix
        if ':role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling' in node.arn:
            service_role_available = True

        if node.instance_profile is not None:
            for launch_config in launch_configs:
                if launch_config['lc_iip'] in node.instance_profile:
                    if node in role_lc_map:
                        role_lc_map[node].append(launch_config['lc_arn'])
                    else:
                        role_lc_map[node] = [launch_config['lc_arn']]

    for node_destination in nodes:
        # check if destination is a user, skip if so
        if ':role/' not in node_destination.arn:
            continue

        # check that the destination role can be assumed by EC2
        sim_result = resource_policy_authorization(
            'ec2.amazonaws.com',
            arns.get_account_id(node_destination.arn),
            node_destination.trust_policy,
            'sts:AssumeRole',
            node_destination.arn,
            {},
        )

        if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
            continue  # EC2 wasn't auth'd to assume the role

        for node_source in nodes:
            # skip self-access checks
            if node_source == node_destination:
                continue

            # check if source is an admin: if so, it can access destination but this is not tracked via an Edge
            if node_source.is_admin:
                continue

            csr_mfa = False  # stash for later ref
            if not service_role_available:
                create_service_role_auth, csr_mfa = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'iam:CreateServiceLinkedRole',
                    '*', {'iam:AWSServiceName': 'autoscaling.amazonaws.com'},
                    service_control_policy_groups=scps)
                if not create_service_role_auth:
                    continue  # service role can't be used if it doesn't exist or be created

            create_auto_scaling_group_auth, casg_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'autoscaling:CreateAutoScalingGroup',
                '*', {},
                service_control_policy_groups=scps)
            if not create_auto_scaling_group_auth:
                continue  # can't create an auto-scaling group -> move along

            if node_destination in role_lc_map:
                if service_role_available:
                    reason = 'can use the EC2 Auto Scaling service role and an existing Launch Configuration to access'
                else:
                    reason = 'can create the EC2 Auto Scaling service role and an existing Launch Configuration to access'

                if csr_mfa or casg_mfa:
                    reason = '(MFA Required) ' + reason

                result.append(
                    Edge(node_source, node_destination, reason,
                         'EC2 Auto Scaling'))

            create_launch_config_auth, clc_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'autoscaling:CreateLaunchConfiguration',
                '*', {},
                service_control_policy_groups=scps)

            if not create_launch_config_auth:
                continue  # we're done here

            pass_role_auth, pr_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'iam:PassRole',
                node_destination.arn,
                {'iam:PassedToService': 'ec2.amazonaws.com'},
                service_control_policy_groups=scps)

            if pass_role_auth:
                if service_role_available:
                    reason = 'can use the EC2 Auto Scaling service role and create a launch configuration to access'
                else:
                    reason = 'can create the EC2 Auto Scaling service role and create a launch configuration to access'
                if clc_mfa or pr_mfa:
                    reason = '(MFA Required) ' + reason

                result.append(
                    Edge(node_source, node_destination, reason,
                         'EC2 Auto Scaling'))

    return result
Example #24
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 #25
0
def generate_edges_locally(
        nodes: List[Node],
        scps: Optional[List[List[dict]]] = None) -> List[Edge]:
    """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code).
    """

    result = []
    for node_destination in nodes:
        if ':role/' not in node_destination.arn:
            continue  # skip non-roles

        for node_source in nodes:
            # skip self-access checks
            if node_source == node_destination:
                continue

            # check if source is an admin, if so it can access destination but this is not tracked via an Edge
            if node_source.is_admin:
                continue

            # Check against resource policy
            sim_result = resource_policy_authorization(
                node_source,
                arns.get_account_id(node_source.arn),
                node_destination.trust_policy,
                'sts:AssumeRole',
                node_destination.arn,
                {},
            )

            if sim_result == ResourcePolicyEvalResult.DENY_MATCH:
                continue  # Node was explicitly denied from assuming the role

            if sim_result == ResourcePolicyEvalResult.NO_MATCH:
                continue  # Resource policy must match for sts:AssumeRole, even in same-account scenarios

            assume_auth, need_mfa = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'sts:AssumeRole',
                node_destination.arn, {},
                service_control_policy_groups=scps)
            policy_denies = has_matching_statement(
                node_source,
                'Deny',
                'sts:AssumeRole',
                node_destination.arn,
                {},
            )
            policy_denies_mfa = has_matching_statement(
                node_source,
                'Deny',
                'sts:AssumeRole',
                node_destination.arn,
                {
                    'aws:MultiFactorAuthAge': '1',
                    'aws:MultiFactorAuthPresent': 'true'
                },
            )

            if assume_auth:
                if need_mfa:
                    reason = '(requires MFA) can access via sts:AssumeRole'
                else:
                    reason = 'can access via sts:AssumeRole'
                new_edge = Edge(node_source, node_destination, reason,
                                'AssumeRole')
                result.append(new_edge)
            elif not (policy_denies_mfa and policy_denies
                      ) and sim_result == ResourcePolicyEvalResult.NODE_MATCH:
                # testing same-account scenario, so NODE_MATCH will override a lack of an allow from iam policy
                new_edge = Edge(node_source, node_destination,
                                'can access via sts:AssumeRole', 'AssumeRole')
                result.append(new_edge)

    return result
Example #26
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 #27
0
def generate_edges_locally(
        nodes: List[Node],
        scps: Optional[List[List[dict]]] = None) -> List[Edge]:
    """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code).
    """

    result = []

    for node_destination in nodes:
        # check if destination is a role with an instance profile
        if ':role/' not in node_destination.arn or node_destination.instance_profile is None:
            continue

        # check if the destination can be assumed by EC2
        sim_result = resource_policy_authorization(
            'ec2.amazonaws.com',
            arns.get_account_id(node_destination.arn),
            node_destination.trust_policy,
            'sts:AssumeRole',
            node_destination.arn,
            {},
        )

        if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH:
            continue  # EC2 wasn't auth'd to assume the role

        # at this point, we make an assumption that some instance is operating with the given instance profile
        # we assume if the role can call ssmmessages:CreateControlChannel, anyone with ssm perms can access it
        if not query_interface.local_check_authorization(
                node_destination, 'ssmmessages:CreateControlChannel', '*', {}):
            continue

        for node_source in nodes:
            # skip self-access checks
            if node_source == node_destination:
                continue

            # check if source is an admin, if so it can access destination but this is not tracked via an Edge
            if node_source.is_admin:
                continue

            # so if source can call ssm:SendCommand or ssm:StartSession, it's an edge
            cmd_auth_res, mfa_res_1 = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'ssm:SendCommand',
                '*',
                {},
            )

            if cmd_auth_res:
                reason = 'can call ssm:SendCommand to access an EC2 instance with access to'
                if mfa_res_1:
                    reason = '(Requires MFA) ' + reason
                result.append(
                    Edge(node_source, node_destination, reason, 'SSM'))

            sesh_auth_res, mfa_res_2 = query_interface.local_check_authorization_handling_mfa(
                node_source,
                'ssm:StartSession',
                '*',
                {},
            )

            if sesh_auth_res:
                reason = 'can call ssm:StartSession to access an EC2 instance with access to'
                if mfa_res_2:
                    reason = '(Requires MFA) ' + reason
                result.append(
                    Edge(node_source, node_destination, reason, 'SSM'))

    return result
Example #28
0
    def return_edges(self,
                     nodes: List[Node],
                     output: io.StringIO = os.devnull,
                     debug: bool = False) -> List[Edge]:
        """Fulfills expected method return_edges. If the session object is None, performs checks in offline-mode"""
        result = []
        for node_source in nodes:
            for node_destination in nodes:
                # skip self-access checks
                if node_source == node_destination:
                    continue

                # check if source is an admin, if so it can access destination but this is not tracked via an Edge
                if node_source.is_admin:
                    continue

                # check if source can call sts:AssumeRole to access the destination if destination is a role
                if ':role/' in node_destination.arn:
                    # Check against resource policy
                    sim_result = resource_policy_authorization(
                        node_source, arns.get_account_id(node_source.arn),
                        node_destination.trust_policy, 'sts:AssumeRole',
                        node_destination.arn, {}, debug)

                    if sim_result == ResourcePolicyEvalResult.DENY_MATCH:
                        continue  # Node was explicitly denied from assuming the role

                    if sim_result == ResourcePolicyEvalResult.NO_MATCH:
                        continue  # Resource policy must match for sts:AssumeRole, even in same-account scenarios

                    assume_auth, need_mfa = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'sts:AssumeRole', node_destination.arn,
                        {}, debug)
                    policy_denies = has_matching_statement(
                        node_source, 'Deny', 'sts:AssumeRole',
                        node_destination.arn, {}, debug)
                    policy_denies_mfa = has_matching_statement(
                        node_source, 'Deny', 'sts:AssumeRole',
                        node_destination.arn, {
                            'aws:MultiFactorAuthAge': '1',
                            'aws:MultiFactorAuthPresent': 'true'
                        }, debug)

                    if assume_auth:
                        if need_mfa:
                            reason = '(requires MFA) can access via sts:AssumeRole'
                        else:
                            reason = 'can access via sts:AssumeRole'
                        new_edge = Edge(node_source, node_destination, reason)
                        output.write('Found new edge: {}\n'.format(
                            new_edge.describe_edge()))
                        result.append(new_edge)
                    elif not (
                            policy_denies_mfa and policy_denies
                    ) and sim_result == ResourcePolicyEvalResult.NODE_MATCH:
                        # testing same-account scenario, so NODE_MATCH will override a lack of an allow from iam policy
                        new_edge = Edge(node_source, node_destination,
                                        'can access via sts:AssumeRole')
                        output.write('Found new edge: {}\n'.format(
                            new_edge.describe_edge()))
                        result.append(new_edge)

        return result