Example #1
0
def _get_admin_reason(node: Node) -> List[str]:
    """Return a list of reasons why this given node is an admin."""

    result = []
    logger.debug("Checking if {} is an admin".format(node.searchable_name()))
    node_type = arns.get_resource(node.arn).split('/')[0]

    # check if node can modify its own inline policies
    if node_type == 'user':
        action = 'iam:PutUserPolicy'
    else:  # node_type == 'role'
        action = 'iam:PutRolePolicy'
    if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, {})[0]:
        result.append('Can call {} to add/update their own inline policies'.format(action))

    # check if node can attach the AdministratorAccess policy to itself
    if node_type == 'user':
        action = 'iam:AttachUserPolicy'
    else:
        action = 'iam:AttachRolePolicy'
    condition_keys = {'iam:PolicyARN': 'arn:aws:iam::aws:policy/AdministratorAccess'}
    if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, condition_keys)[0]:
        result.append('Can call {} to attach the AdministratorAccess policy to itself'.format(action))

    # check if node can create a role and attach the AdministratorAccess policy or an inline policy
    if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreateRole', '*', {})[0]:
        if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachRolePolicy', '*',
                                                                  condition_keys)[0]:
            result.append('Can create an IAM Role (iam:CreateRole) and attach the AdministratorAccess policy to it (iam:AttachRolePolicy)'.format(action))
        if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutRolePolicy', '*', condition_keys)[0]:
            result.append('Can create an IAM Role (iam:CreateRole) and create an inline policy for it (iam:PutRolePolicy)'.format(action))

    # check if node can update an attached customer-managed policy (assumes SetAsDefault is set to True)
    for attached_policy in node.attached_policies:
        if attached_policy.arn != node.arn and ':aws:policy/' not in attached_policy.arn:
            if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion',
                                                                      attached_policy.arn, {})[0]:
                result.append('Can modify the attached managed policy {} (iam:CreatePolicyVersion)'.format(attached_policy.arn))
                break  # reduce output

    # check if node is a user, and if it can attach or modify any of its groups's policies
    if node_type == 'user':
        for group in node.group_memberships:
            group_name = group.arn.split('/')[-1]

            if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutGroupPolicy', group.arn, {})[0]:
                result.append('Can add/update an inline policy for the group {} (iam:PutGroupPolicy)'.format(group_name))

            if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachGroupPolicy', group.arn,
                                                                      condition_keys)[0]:
                result.append('Can attach the AdministratorAccess policy to the group {} (iam:AttachGroupPolicy)'.format(group_name))

            for attached_policy in group.attached_policies:
                if attached_policy.arn != group.arn and ':aws:policy/' not in attached_policy.arn:
                    if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion',
                                                                              attached_policy.arn, {})[0]:
                        result.append('Can update the managed policy {} that is attached to the group {} (iam:CreatePolicyVersion)'.format(attached_policy.arn, group_name))
                        break  # reduce output

    return result
Example #2
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
Example #3
0
def _can_call_without_mfa(node: Node, actions: List[str]) -> bool:
    """Returns true if node can call sensitive action without MFA"""
    for action in actions:
        auth, needmfa = query_interface.local_check_authorization_handling_mfa(
            node, action, '*', {})
        if auth and not needmfa:
            return True
    return False
Example #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
Example #5
0
def update_admin_status(nodes: List[Node],
                        output: io.StringIO = os.devnull,
                        debug: bool = False) -> None:
    """Given a list of nodes, goes through and updates each node's is_admin data."""
    for node in nodes:
        output.write("checking if {} is an admin\n".format(
            node.searchable_name()))
        node_type = arns.get_resource(node.arn).split('/')[0]

        # check if node can modify its own inline policies
        if node_type == 'user':
            action = 'iam:PutUserPolicy'
        else:  # node_type == 'role'
            action = 'iam:PutRolePolicy'
        if query_interface.local_check_authorization_handling_mfa(
                node, action, node.arn, {}, debug)[0]:
            node.is_admin = True
            continue

        # check if node can attach the AdministratorAccess policy to itself
        if node_type == 'user':
            action = 'iam:AttachUserPolicy'
        else:
            action = 'iam:AttachRolePolicy'
        condition_keys = {
            'iam:PolicyARN': 'arn:aws:iam::aws:policy/AdministratorAccess'
        }
        if query_interface.local_check_authorization_handling_mfa(
                node, action, node.arn, condition_keys, debug)[0]:
            node.is_admin = True
            continue

        # check if node can create a role and attach the AdministratorAccess policy or an inline policy
        if query_interface.local_check_authorization_handling_mfa(
                node, 'iam:CreateRole', '*', {}, debug)[0]:
            if query_interface.local_check_authorization_handling_mfa(
                    node, 'iam:AttachRolePolicy', '*', condition_keys,
                    debug)[0]:
                node.is_admin = True
                continue
            if query_interface.local_check_authorization_handling_mfa(
                    node, 'iam:PutRolePolicy', '*', condition_keys, debug)[0]:
                node.is_admin = True
                continue

        # check if node can update an attached customer-managed policy (assumes SetAsDefault is set to True)
        for attached_policy in node.attached_policies:
            if attached_policy.arn != node.arn:
                if query_interface.local_check_authorization_handling_mfa(
                        node, 'iam:CreatePolicyVersion', attached_policy.arn,
                    {}, debug)[0]:
                    node.is_admin = True
                    continue

        # check if node is a user, and if it can attach or modify any of its groups's policies
        if node_type == 'user':
            for group in node.group_memberships:
                if query_interface.local_check_authorization_handling_mfa(
                        node, 'iam:PutGroupPolicy', group.arn, {}, debug)[0]:
                    node.is_admin = True
                    break  # break the loop through groups
                if query_interface.local_check_authorization_handling_mfa(
                        node, 'iam:AttachGroupPolicy', group.arn,
                        condition_keys, debug)[0]:
                    node.is_admin = True
                    break  # as above
                for attached_policy in group.attached_policies:
                    if attached_policy.arn != group.arn:
                        if query_interface.local_check_authorization_handling_mfa(
                                node, 'iam:CreatePolicyVersion',
                                attached_policy.arn, {}, debug)[0]:
                            node.is_admin = True
                            break  # break the loop through policies
                if node.is_admin:
                    break  # break the loop through groups
            if node.is_admin:
                continue  # if we add more checks later, this optimizes them out when appropriate
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 #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:
        # 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 #8
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
Example #9
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 #10
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
Example #11
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
Example #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
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 #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 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
Example #15
0
    def test_local_mfa_handling(self):
        mfa_policy = Policy(
            arn='arn:aws:iam::000000000000:policy/mfa_policy',
            name='mfa_policy',
            policy_doc={
                "Version":
                "2012-10-17",
                "Statement": [{
                    "Sid":
                    "AllowManageOwnUserMFA",
                    "Effect":
                    "Allow",
                    "Action": [
                        "iam:DeactivateMFADevice", "iam:EnableMFADevice",
                        "iam:GetUser", "iam:ListMFADevices",
                        "iam:ResyncMFADevice"
                    ],
                    "Resource":
                    "arn:aws:iam::*:user/${aws:username}"
                }, {
                    "Sid":
                    "DenyAllExceptListedIfNoMFA",
                    "Effect":
                    "Deny",
                    "NotAction": [
                        "iam:CreateVirtualMFADevice", "iam:EnableMFADevice",
                        "iam:GetUser", "iam:ListMFADevices",
                        "iam:ListVirtualMFADevices", "iam:ResyncMFADevice",
                        "sts:GetSessionToken"
                    ],
                    "Resource":
                    "*",
                    "Condition": {
                        "BoolIfExists": {
                            "aws:MultiFactorAuthPresent": "false"
                        }
                    }
                }]
            })
        s3_policy = Policy(
            'arn:aws:iam::000000000000:policy/s3access', 's3access', {
                "Version":
                "2012-10-17",
                "Statement": [{
                    "Sid": "AllowViewAccountInfo",
                    "Effect": "Allow",
                    "Action": "s3:ListAllMyBuckets",
                    "Resource": "*"
                }]
            })
        test_node = Node('arn:aws:iam::000000000000:user/uses_mfa',
                         'AIDA00000000000000000', [mfa_policy, s3_policy], [],
                         None, None, 1, False, False, None, True, None)

        print(mfa_policy.to_dictionary())
        print(s3_policy.to_dictionary())
        print(test_node.to_dictionary())

        # Test that lack of MFA conditions is not allowed (note the function call diff)
        # This matches policy sim feedback
        auth_result = local_check_authorization(test_node,
                                                's3:ListAllMyBuckets', '*', {})
        self.assertFalse(auth_result)

        # Test that MFA set to false is disallowed
        auth_result, mfa_result = local_check_authorization_handling_mfa(
            test_node, 's3:ListAllMyBuckets', '*',
            {'aws:MultiFactorAuthPresent': 'false'})
        self.assertFalse(auth_result)
        self.assertFalse(mfa_result)

        # Test that testing both with and without MFA yields correct results
        auth_result, mfa_result = local_check_authorization_handling_mfa(
            test_node, 's3:ListAllMyBuckets', '*', {})
        self.assertTrue(auth_result)
        self.assertTrue(mfa_result)

        # Test that iam:EnableMFADevice is allowed despite lack of MFA
        auth_result, mfa_result = local_check_authorization_handling_mfa(
            test_node, 'iam:EnableMFADevice',
            'arn:aws:iam::000000000000:user/uses_mfa', {})

        self.assertFalse(mfa_result)
        self.assertTrue(auth_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 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 #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 = []

        # 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 #18
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 #19
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