예제 #1
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 = []
        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 role with an instance profile
                if ':role/' not in node_destination.arn or node_destination.instance_profile is None:
                    continue

                # 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',
                        '*', {}, False):
                    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', '*', {}, False)

                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))

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

                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))

        for edge in result:
            output.write("Found new edge: {}\n".format(edge.describe_edge()))
        return result
예제 #2
0
def get_search_list(graph: Graph, 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 of nodes from a source node in a graph.
    """
    result = []
    explored_nodes = []

    # Special-case: node is an "admin", so we make up admin edges and return them all
    if node.is_admin:
        for other_node in graph.nodes:
            if node == other_node:
                continue
            result.append([Edge(node, other_node, 'can access through administrative actions', 'Admin')])
        return result

    # run through initial edges
    for edge in get_edges_with_node_source(graph, node, explored_nodes):
        result.append([edge])
    explored_nodes.append(node)

    # dig through result list
    index = 0
    while index < len(result):
        current_node = result[index][-1].destination
        for edge in get_edges_with_node_source(graph, current_node, explored_nodes):
            result.append(result[index][:] + [edge])
        explored_nodes.append(current_node)
        index += 1

    return result
예제 #3
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
예제 #4
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
예제 #5
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
예제 #6
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
예제 #7
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
예제 #8
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
예제 #9
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
예제 #10
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_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

            if ':user/' in node_destination.arn:
                # Change the user's access keys
                access_keys_mfa = False

                create_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'iam:CreateAccessKey',
                    node_destination.arn,
                    {},
                    service_control_policy_groups=scps
                )

                if mfa_res:
                    access_keys_mfa = True

                if node_destination.access_keys == 2:
                    # can have a max of two access keys, need to delete before making a new one
                    auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'iam:DeleteAccessKey',
                        node_destination.arn,
                        {},
                        service_control_policy_groups=scps
                    )
                    if not auth_res:
                        create_auth_res = False  # can't delete target access key, can't generate a new one
                    if mfa_res:
                        access_keys_mfa = True

                if create_auth_res:
                    reason = 'can create access keys to authenticate as'
                    if access_keys_mfa:
                        reason = '(MFA required) ' + reason

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

                # Change the user's password
                if node_destination.active_password:
                    pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'iam:UpdateLoginProfile',
                        node_destination.arn,
                        {},
                        service_control_policy_groups=scps
                    )
                else:
                    pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source,
                        'iam:CreateLoginProfile',
                        node_destination.arn,
                        {},
                        service_control_policy_groups=scps
                    )
                if pass_auth_res:
                    reason = 'can set the password to authenticate as'
                    if mfa_res:
                        reason = '(MFA required) ' + reason
                    result.append(Edge(node_source, node_destination, reason, 'IAM'))

            if ':role/' in node_destination.arn:
                # Change the role's trust doc
                update_role_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                    node_source,
                    'iam:UpdateAssumeRolePolicy',
                    node_destination.arn,
                    {},
                    service_control_policy_groups=scps
                )
                if update_role_res:
                    reason = 'can update the trust document to access'
                    if mfa_res:
                        reason = '(MFA required) ' + reason
                    result.append(Edge(node_source, node_destination, reason, 'IAM'))

    return result
예제 #11
0
def get_edges_between_graphs(
        graph_a: Graph,
        graph_b: Graph,
        scps_a: Optional[List[List[dict]]] = None,
        scps_b: Optional[List[List[dict]]] = None) -> List[Edge]:
    """Given two Graph objects, return a list of Edge objects that represent the connections between
    the two Graphs (both to and from). Currently only does sts:AssumeRole checks."""

    result = []  # type: List[Edge]

    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

    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()))

    for node_a in graph_a.nodes:
        for node_b in graph_b.nodes:
            # check a -> b
            if node_b.searchable_name().startswith('role/'):
                if _check_assume_role(graph_a, node_a, graph_b, node_b,
                                      scps_a):
                    logger.info('Found edge: {}'.format(
                        _describe_edge(node_a, node_b)))
                    result.append(
                        Edge(node_a, node_b,
                             'can call sts:AssumeRole to access', 'STS'))

            # check b -> a
            if node_a.searchable_name().startswith('role/'):
                if _check_assume_role(graph_b, node_b, graph_a, node_a,
                                      scps_b):
                    logger.info('Found edge: {}'.format(
                        _describe_edge(node_b, node_a)))
                    result.append(
                        Edge(node_b, node_a,
                             'can call sts:AssumeRole to access', 'STS'))

    return result
예제 #12
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
예제 #13
0
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
예제 #14
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
예제 #15
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
예제 #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:
        # 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
예제 #17
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

                if ':user/' in node_destination.arn:
                    # Change the user's access keys
                    access_keys_mfa = False

                    create_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'iam:CreateAccessKey',
                        node_destination.arn, {}, debug)

                    if mfa_res:
                        access_keys_mfa = True

                    if node_destination.access_keys == 2:
                        # can have a max of two access keys, need to delete before making a new one
                        auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                            node_source, 'iam:DeleteAccessKey',
                            node_destination.arn, {}, debug)
                        if not auth_res:
                            create_auth_res = False  # can't delete target access key, can't generate a new one
                        if mfa_res:
                            access_keys_mfa = True

                    if create_auth_res:
                        reason = 'can create access keys to authenticate as'
                        if access_keys_mfa:
                            reason = '(MFA required) ' + reason

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

                    # Change the user's password
                    if node_destination.active_password:
                        pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                            node_source, 'iam:UpdateLoginProfile',
                            node_destination.arn, {}, debug)
                    else:
                        pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                            node_source, 'iam:CreateLoginProfile',
                            node_destination.arn, {}, debug)
                    if pass_auth_res:
                        reason = 'can set the password to authenticate as'
                        if mfa_res:
                            reason = '(MFA required) ' + reason
                        result.append(
                            Edge(node_source, node_destination, reason))

                if ':role/' in node_destination.arn:
                    # Change the role's trust doc
                    update_role_res, mfa_res = query_interface.local_check_authorization_handling_mfa(
                        node_source, 'iam:UpdateAssumeRolePolicy',
                        node_destination.arn, {}, debug)
                    if update_role_res:
                        reason = 'can update the trust document to access'
                        if mfa_res:
                            reason = '(MFA required) ' + reason
                        result.append(
                            Edge(node_source, node_destination, reason))

        for edge in result:
            output.write("Found new edge: {}\n".format(edge.describe_edge()))
        return result