|
| 1 | +# Copyright (c) NCC Group and Erik Steringer 2021. This file is part of Principal Mapper. |
| 2 | +# |
| 3 | +# Principal Mapper is free software: you can redistribute it and/or modify |
| 4 | +# it under the terms of the GNU Affero General Public License as published by |
| 5 | +# the Free Software Foundation, either version 3 of the License, or |
| 6 | +# (at your option) any later version. |
| 7 | +# |
| 8 | +# Principal Mapper is distributed in the hope that it will be useful, |
| 9 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | +# GNU Affero General Public License for more details. |
| 12 | +# |
| 13 | +# You should have received a copy of the GNU Affero General Public License |
| 14 | +# along with Principal Mapper. If not, see <https://www.gnu.org/licenses/>. |
| 15 | + |
| 16 | +import copy |
| 17 | +import json |
| 18 | +import logging |
| 19 | +import re |
| 20 | +from typing import List, Dict, Tuple, Optional |
| 21 | + |
| 22 | +from principalmapper.common import Graph, Policy, Node |
| 23 | +from principalmapper.querying import query_interface |
| 24 | +from principalmapper.querying.local_policy_simulation import policy_has_matching_statement |
| 25 | +from principalmapper.util import arns |
| 26 | +from principalmapper.util.case_insensitive_dict import CaseInsensitiveDict |
| 27 | + |
| 28 | + |
| 29 | +logger = logging.getLogger(__name__) |
| 30 | + |
| 31 | + |
| 32 | +def handle_preset_query(graph: Graph, tokens: List[str], skip_admins: bool = False) -> None: |
| 33 | + """Handles a human-readable query that's been chunked into tokens, and prints the results. Prints out the |
| 34 | + principals in the account who are marked Admin but do not have the AdministratorAccess managed policy or an |
| 35 | + inline equivalent. |
| 36 | +
|
| 37 | + Tokens should be: |
| 38 | +
|
| 39 | + * "preset" |
| 40 | + * "wrongadmin" |
| 41 | + """ |
| 42 | + |
| 43 | + wal = compose_wrong_admin_list(graph) |
| 44 | + for node, reasons in wal: |
| 45 | + print('{}'.format(node.searchable_name())) |
| 46 | + for reason in reasons: |
| 47 | + print(' * {}'.format(reason)) |
| 48 | + print() |
| 49 | + |
| 50 | + |
| 51 | +def _is_admin_or_equiv_policy(policy: Policy) -> bool: |
| 52 | + """Given a Policy return true if the policy is the AdministratorAccess policy or if the policy document |
| 53 | + is effectively the same.""" |
| 54 | + |
| 55 | + if ':aws:policy/AdministratorAccess' in policy.arn: |
| 56 | + return True |
| 57 | + |
| 58 | + for stmt in policy.policy_doc['Statement']: |
| 59 | + action_flag, resource_flag = False, False |
| 60 | + if 'Action' in stmt and 'Resource' in stmt and stmt['Effect'] == 'Allow': |
| 61 | + if isinstance(stmt['Action'], str): |
| 62 | + if stmt['Action'] == '*': |
| 63 | + action_flag = True |
| 64 | + else: |
| 65 | + for action in stmt['Action']: |
| 66 | + if action == '*': |
| 67 | + action_flag = True |
| 68 | + break |
| 69 | + |
| 70 | + if isinstance(stmt['Resource'], str): |
| 71 | + if stmt['Resource'] == '*': |
| 72 | + resource_flag = True |
| 73 | + else: |
| 74 | + for resource in stmt['Resource']: |
| 75 | + if resource == '*': |
| 76 | + resource_flag = True |
| 77 | + break |
| 78 | + if action_flag and resource_flag: |
| 79 | + return True |
| 80 | + |
| 81 | + return False |
| 82 | + |
| 83 | + |
| 84 | +def _get_admin_reason(node: Node) -> List[str]: |
| 85 | + """Return a list of reasons why this given node is an admin.""" |
| 86 | + |
| 87 | + result = [] |
| 88 | + logger.debug("Checking if {} is an admin".format(node.searchable_name())) |
| 89 | + node_type = arns.get_resource(node.arn).split('/')[0] |
| 90 | + |
| 91 | + # check if node can modify its own inline policies |
| 92 | + if node_type == 'user': |
| 93 | + action = 'iam:PutUserPolicy' |
| 94 | + else: # node_type == 'role' |
| 95 | + action = 'iam:PutRolePolicy' |
| 96 | + if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, {})[0]: |
| 97 | + result.append('Can call {} to add/update their own inline policies'.format(action)) |
| 98 | + |
| 99 | + # check if node can attach the AdministratorAccess policy to itself |
| 100 | + if node_type == 'user': |
| 101 | + action = 'iam:AttachUserPolicy' |
| 102 | + else: |
| 103 | + action = 'iam:AttachRolePolicy' |
| 104 | + condition_keys = {'iam:PolicyARN': 'arn:aws:iam::aws:policy/AdministratorAccess'} |
| 105 | + if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, condition_keys)[0]: |
| 106 | + result.append('Can call {} to attach the AdministratorAccess policy to itself'.format(action)) |
| 107 | + |
| 108 | + # check if node can create a role and attach the AdministratorAccess policy or an inline policy |
| 109 | + if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreateRole', '*', {})[0]: |
| 110 | + if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachRolePolicy', '*', |
| 111 | + condition_keys)[0]: |
| 112 | + result.append('Can create an IAM Role (iam:CreateRole) and attach the AdministratorAccess policy to it (iam:AttachRolePolicy)'.format(action)) |
| 113 | + if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutRolePolicy', '*', condition_keys)[0]: |
| 114 | + result.append('Can create an IAM Role (iam:CreateRole) and create an inline policy for it (iam:PutRolePolicy)'.format(action)) |
| 115 | + |
| 116 | + # check if node can update an attached customer-managed policy (assumes SetAsDefault is set to True) |
| 117 | + for attached_policy in node.attached_policies: |
| 118 | + if attached_policy.arn != node.arn and ':aws:policy/' not in attached_policy.arn: |
| 119 | + if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion', |
| 120 | + attached_policy.arn, {})[0]: |
| 121 | + result.append('Can modify the attached managed policy {} (iam:CreatePolicyVersion)'.format(attached_policy.arn)) |
| 122 | + break # reduce output |
| 123 | + |
| 124 | + # check if node is a user, and if it can attach or modify any of its groups's policies |
| 125 | + if node_type == 'user': |
| 126 | + for group in node.group_memberships: |
| 127 | + group_name = group.arn.split('/')[-1] |
| 128 | + |
| 129 | + if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutGroupPolicy', group.arn, {})[0]: |
| 130 | + result.append('Can add/update an inline policy for the group {} (iam:PutGroupPolicy)'.format(group_name)) |
| 131 | + |
| 132 | + if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachGroupPolicy', group.arn, |
| 133 | + condition_keys)[0]: |
| 134 | + result.append('Can attach the AdministratorAccess policy to the group {} (iam:AttachGroupPolicy)'.format(group_name)) |
| 135 | + |
| 136 | + for attached_policy in group.attached_policies: |
| 137 | + if attached_policy.arn != group.arn and ':aws:policy/' not in attached_policy.arn: |
| 138 | + if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion', |
| 139 | + attached_policy.arn, {})[0]: |
| 140 | + result.append('Can update the managed policy {} that is attached to the group {} (iam:CreatePolicyVersion)'.format(attached_policy.arn, group_name)) |
| 141 | + break # reduce output |
| 142 | + |
| 143 | + return result |
| 144 | + |
| 145 | + |
| 146 | +def compose_wrong_admin_list(graph: Graph) -> List[Tuple[Node, List[str]]]: |
| 147 | + """Given a Graph, return the collection of principals that are admins but do not have the |
| 148 | + AdminstratorAccess policy or an equivalent inline policy, along with a list of what policy/policies |
| 149 | + and statements that could be the source of the admin-access.""" |
| 150 | + |
| 151 | + result = [] |
| 152 | + |
| 153 | + # iterate through all nodes |
| 154 | + for node in graph.nodes: |
| 155 | + |
| 156 | + # skip non-admins |
| 157 | + if not node.is_admin: |
| 158 | + continue |
| 159 | + |
| 160 | + # skip principals with Admin or equiv policy |
| 161 | + flag = False |
| 162 | + for attached_policy in node.attached_policies: |
| 163 | + if _is_admin_or_equiv_policy(attached_policy): |
| 164 | + flag = True |
| 165 | + break |
| 166 | + if flag: |
| 167 | + continue |
| 168 | + |
| 169 | + # skip IAM Users in IAM Groups with Admin or equiv policy |
| 170 | + if ':user/' in node.arn: |
| 171 | + flag = False |
| 172 | + for group in node.group_memberships: |
| 173 | + for attached_policy in group.attached_policies: |
| 174 | + if _is_admin_or_equiv_policy(attached_policy): |
| 175 | + flag = True |
| 176 | + break |
| 177 | + if flag: |
| 178 | + break |
| 179 | + if flag: |
| 180 | + continue |
| 181 | + |
| 182 | + # at this point we have a node that's an admin, so let's find the potentially responsible statements |
| 183 | + result.append((node, _get_admin_reason(node))) |
| 184 | + |
| 185 | + return result |
0 commit comments