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
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
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
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
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
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
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
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
def generate_edges_locally( nodes: List[Node], scps: Optional[List[List[dict]]] = None, codebuild_projects: Optional[List[dict]] = None) -> List[Edge]: """Generates and returns Edge objects related to AWS CodeBuild. It is possible to use this method if you are operating offline (infra-as-code). The `codebuild_projects` param should be a list of dictionary objects with the following expected structure: ``` { 'project_arn': <str: ARN of a project>, 'project_role': <str: ARN of a role attached to a project> 'project_tags': <list[dict]: tags for the project as in [{'Key': <Key>, 'Value': <Value>}]> } ``` All elements are required, tags must point to an empty list if there are no tags attached to the project """ result = [] # we wanna create a role -> [{proj_arn: <>, proj_tags: <>}] map to make eventual lookups faster if codebuild_projects is None: codebuild_map = {} else: codebuild_map = {} # type: Dict[str, List[dict]] for project in codebuild_projects: if project['project_role'] not in codebuild_map: codebuild_map[project['project_role']] = [{ 'proj_arn': project['project_arn'], 'proj_tags': project['project_tags'] }] else: codebuild_map[project['project_role']].append({ 'proj_arn': project['project_arn'], 'proj_tags': project['project_tags'] }) for node_destination in nodes: # check if destination is a user, skip if so if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by CodeBuild sim_result = resource_policy_authorization( 'codebuild.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, ) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # CodeBuild wasn't auth'd to assume the role for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin: if so, it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check if source can use existing projects if node_destination.arn in codebuild_map: projects = codebuild_map[node_destination.arn] for project in projects: startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuild', project['proj_arn'], _gen_resource_tag_conditions(project['proj_tags']), service_control_policy_groups=scps) if startproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can use CodeBuild with an existing project to access' if startproj_mfa else 'can use CodeBuild with an existing project to access', 'CodeBuild')) break # break out of iterating through projects batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuildBatch', project['proj_arn'], _gen_resource_tag_conditions(project['proj_tags']), service_control_policy_groups=scps) if batchstartproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can use CodeBuild with an existing project to access' if startproj_mfa else 'can use CodeBuild with an existing project to access', 'CodeBuild')) break # break out of iterating through projects # check if source can create/update a project, pass this role, then start a build condition_keys = {'iam:PassedToService': 'codebuild.amazonaws.com'} pass_role_auth, pass_role_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, condition_keys, service_control_policy_groups=scps) if not pass_role_auth: continue # if we can't pass this role, then we're done # check if the source can create a project and start a build create_proj_auth, create_proj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:CreateProject', '*', {}, service_control_policy_groups=scps) if create_proj_auth: startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuild', '*', {}, service_control_policy_groups=scps) if startproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can create a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can create a project in CodeBuild to access', 'CodeBuild')) else: batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuildBatch', '*', {}, service_control_policy_groups=scps) if batchstartproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can create a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can create a project in CodeBuild to access', 'CodeBuild')) # check if the source can update a project and start a build for project in codebuild_projects: update_proj_auth, update_proj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:UpdateProject', project['project_arn'], _gen_resource_tag_conditions(project['project_tags']), service_control_policy_groups=scps) if update_proj_auth: startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuild', project['project_arn'], _gen_resource_tag_conditions(project['project_tags']), service_control_policy_groups=scps) if startproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can update a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can update a project in CodeBuild to access', 'CodeBuild')) break # just wanna find that there exists one updatable/usable project else: batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuildBatch', project['project_arn'], _gen_resource_tag_conditions( project['project_tags']), service_control_policy_groups=scps) if batchstartproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can update a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can update a project in CodeBuild to access', 'CodeBuild')) break # just wanna find that there exists one updatable/usable project return result
def generate_edges_locally(nodes: List[Node], 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
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
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
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
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
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
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