def test_get_actions_from_statement(self): statement = { "Action": "ec2:thispermissiondoesntexist", "NotAction": list(all_permissions), "Resource": "*", "Effect": "Allow", } expected_result = {"ec2:thispermissiondoesntexist"} result = get_actions_from_statement(statement) self.assertEqual(result, expected_result) result = get_actions_from_statement(dict(NotAction="abc")) self.assertSetEqual(result, set(all_permissions)) statement = { "Action": ( "ec2:updatesecuritygroupruledescriptionsegress", "ec2:cancelcapacityreservation", ), "NotAction": tuple(), "Resource": "*", "Effect": "Allow", } result = get_actions_from_statement(statement) self.assertSetEqual( result, { "ec2:updatesecuritygroupruledescriptionsegress", "ec2:cancelcapacityreservation", }, )
def test_get_actions_from_statement(self): statement = { "Action": "ec2:thispermissiondoesntexist", "NotAction": list(all_permissions), "Resource": "*", "Effect": "Allow" } expected_result = {"ec2:thispermissiondoesntexist"} result = get_actions_from_statement(statement) self.assertEqual(result, expected_result) get_actions_from_statement(dict(NotAction="abc"))
def get_permissions_in_policy( policy_dict: Dict[str, Any], warn_unknown_perms: bool = False) -> Tuple[Set[str], Set[str]]: """ Given a set of policies for a role, return a set of all allowed permissions Args: policy_dict warn_unknown_perms Returns tuple set - all permissions allowed by the policies set - all permisisons allowed by the policies not marked with STATEMENT_SKIP_SID """ total_permissions: Set[str] = set() eligible_permissions: Set[str] = set() for policy_name, policy in list(policy_dict.items()): policy = expand_policy(policy=policy, expand_deny=False) if policy else {} for statement in policy.get("Statement"): if statement["Effect"].lower() == "allow": statement_actions = get_actions_from_statement(statement) total_permissions = total_permissions.union(statement_actions) if not ("Sid" in statement and statement["Sid"].startswith(STATEMENT_SKIP_SID)): # No Sid # Sid exists, but doesn't start with STATEMENT_SKIP_SID eligible_permissions = eligible_permissions.union( statement_actions) weird_permissions = total_permissions.difference(all_permissions) if weird_permissions and warn_unknown_perms: logger.warning( "Unknown permissions found: {}".format(weird_permissions)) return total_permissions, eligible_permissions
def actions_expanded(self): return set(get_actions_from_statement(self.statement))
def pass_condition(b, test, a): """ Generic test function used by Scout . :param b: Value to be tested against :param test: Name of the test case to run :param a: Value to be tested :return: True of condition is met, False otherwise """ # Return false by default result = False # Equality tests if test == 'equal': a = str(a) b = str(b) result = (a == b) elif test == 'notEqual': result = (not pass_condition(b, 'equal', a)) # More/Less tests elif test == 'lessThan': result = (int(b) < int(a)) elif test == 'lessOrEqual': result = (int(b) <= int(a)) elif test == 'moreThan': result = (int(b) > int(a)) elif test == 'moreOrEqual': result = (int(b) >= int(a)) # Empty tests elif test == 'empty': result = ((type(b) == dict and b == {}) or (type(b) == list and b == []) or (type(b) == list and b == [None])) elif test == 'notEmpty': result = (not pass_condition(b, 'empty', 'a')) elif test == 'null': result = ((b is None) or (type(b) == str and b == 'None')) elif test == 'notNull': result = (not pass_condition(b, 'null', a)) # Boolean tests elif test == 'true': result = (str(b).lower() == 'true') elif test == 'notTrue' or test == 'false': result = (str(b).lower() == 'false') # Object length tests elif test == 'lengthLessThan': result = (len(b) < int(a)) elif test == 'lengthMoreThan': result = (len(b) > int(a)) elif test == 'lengthEqual': result = (len(b) == int(a)) # Dictionary keys tests elif test == 'withKey': result = (a in b) elif test == 'withoutKey': result = a not in b # String test elif test == 'containString': if not type(b) == str: b = str(b) if not type(a) == str: a = str(a) result = a in b elif test == 'notContainString': if not type(b) == str: b = str(b) if not type(a) == str: a = str(a) result = a not in b # List tests elif test == 'containAtLeastOneOf': result = False if not type(b) == list: b = [b] if not type(a) == list: a = [a] for c in b: if type(c) != dict: c = str(c) if c in a: result = True break elif test == 'containAtLeastOneDifferentFrom': result = False if not type(b) == list: b = [b] if not type(a) == list: a = [a] for c in b: if c and c != '' and c not in a: result = True break elif test == 'containNoneOf': result = True if not type(b) == list: b = [b] if not type(a) == list: a = [a] for c in b: if c in a: result = False break elif test == 'containAtLeastOneMatching': result = False for item in b: if re.match(a, item): result = True break # Regex tests elif test == 'match': if type(a) != list: a = [a] b = str(b) for c in a: if re.match(c, b): result = True break elif test == 'notMatch': result = (not pass_condition(b, 'match', a)) # Date tests elif test == 'priorToDate': b = dateutil.parser.parse(str(b)).replace(tzinfo=None) a = dateutil.parser.parse(str(a)).replace(tzinfo=None) result = (b < a) elif test == 'olderThan': age, threshold = __prepare_age_test(a, b) result = (age > threshold) elif test == 'newerThan': age, threshold = __prepare_age_test(a, b) result = (age < threshold) # CIDR tests elif test == 'inSubnets': result = False grant = netaddr.IPNetwork(b) if type(a) != list: a = [a] for c in a: known_subnet = netaddr.IPNetwork(c) if grant in known_subnet: result = True break elif test == 'notInSubnets': result = (not pass_condition(b, 'inSubnets', a)) # Policy statement tests elif test == 'containAction': result = False if type(b) != dict: b = json.loads(b) statement_actions = get_actions_from_statement(b) rule_actions = _expand_wildcard_action(a) for action in rule_actions: if action.lower() in statement_actions: result = True break elif test == 'notContainAction': result = (not pass_condition(b, 'containAction', a)) elif test == 'containAtLeastOneAction': result = False if type(b) != dict: b = json.loads(b) if type(a) != list: a = [a] actions = get_actions_from_statement(b) for c in a: if c.lower() in actions: result = True break # Policy principal tests elif test == 'isCrossAccount': result = False if type(b) != list: b = [b] for c in b: if type(c) == dict and 'AWS' in c: c = c['AWS'] if c != a and not re.match(r'arn:aws:iam:.*?:%s:.*' % a, c): result = True break elif test == 'isSameAccount': result = False if type(b) != list: b = [b] for c in b: if c == a or re.match(r'arn:aws:iam:.*?:%s:.*' % a, c): result = True break # Unknown test case else: print_error('Error: unknown test case %s' % test) raise Exception return result
def get_repoed_policy( policies: Dict[str, Any], repoable_permissions: Set[str]) -> Tuple[Dict[str, Any], List[str]]: """ This function contains the logic to rewrite the policy to remove any repoable permissions. To do so we: - Iterate over role policies - Iterate over policy statements - Skip Deny statements - Remove any actions that are in repoable_permissions - Remove any statements that now have zero actions - Remove any policies that now have zero statements Args: policies (dict): All of the inline policies as a dict with name and policy contents repoable_permissions (set): A set of all of the repoable permissions for policies Returns: dict: The rewritten set of all inline policies list: Any policies that are now empty as a result of the rewrites """ # work with our own copy; don't mess with the CACHE copy. role_policies = copy.deepcopy(policies) empty_policies = [] for policy_name, policy in list(role_policies.items()): # list of indexes in the policy that are empty empty_statements = [] if type(policy["Statement"]) is dict: policy["Statement"] = [policy["Statement"]] for idx, statement in enumerate(policy["Statement"]): if statement["Effect"].lower() == "allow": if "Sid" in statement and statement["Sid"].startswith( STATEMENT_SKIP_SID): continue statement_actions = get_actions_from_statement(statement) new_actions = { action for action in statement_actions if action not in repoable_permissions and action.split(":")[0] not in repoable_permissions } if statement_actions == new_actions: # No permissions are being taken away; let's not modify this statement at all. continue # get_actions_from_statement has already inverted this so our new statement should be 'Action' if "NotAction" in statement: del statement["NotAction"] # by putting this into a set, we lose order, which may be confusing to someone. statement["Action"] = sorted(list(new_actions)) # mark empty statements to be removed if len(statement["Action"]) == 0: empty_statements.append(idx) # do the actual removal of empty statements for idx in sorted(empty_statements, reverse=True): del policy["Statement"][idx] # mark empty policies to be removed if len(policy["Statement"]) == 0: empty_policies.append(policy_name) # do the actual removal of empty policies. for policy_name in empty_policies: del role_policies[policy_name] return role_policies, empty_policies
def getPolicyStatementDetails(statement): ''' These are the different element of a Policy Statement i. Action / NotAction ii. Effect iii. Resource / NotResource iv. Sid v. Condition vi. Principal / NotPrincipal Link : https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html This function parses the policy statement and gives values for all possible elements of a policy in a standard key value format ''' # Determining Actions try: statement_action = statement['Action'] statement_action_key = "Action" except KeyError: statement_action = statement['NotAction'] statement_action_key = "NotAction" # Stringifying and Replacing list and set characters helps in linearising the lists / sets # PolicyStatement Relationships contain actions as one of the properties # Hence sorting is important , as every time the order keeps changing when the string is split. # Sorting helps from creating duplicate relationships when the data is synced again statement_action = sorted( set( str(statement_action).replace("'", "").replace("{", "").replace( "}", "").replace("[", "").replace("]", "").replace(" ", "").split(","))) # Determining Resource try: statement_resource = statement['Resource'] statement_resource_key = "Resource" except KeyError: try: statement_resource = statement['NotResource'] statement_resource_key = "NotResource" # In case there is no Resource (AssumeRole Policies do not have Resource mentioned) except KeyError: statement_resource = set() statement_resource_key = "" # Stringifying and Replacing list and set characters helps in linearising the lists / sets # PolicyStatement Relationships contain resources as one of the properties # Hence sorting is important , as every time the order keeps changing when the string is split. # Sorting helps from creating duplicate relationships when the data is synced again if statement_resource != set(): statement_resource = sorted( set( str(statement_resource).replace("'", "").replace( "{", "").replace("}", "").replace("[", "").replace( "]", "").replace(" ", "").split(","))) # Determining Effect statement_effect = statement['Effect'] # Determining Principal # Principals are not part of every type of AWS Policy (Hence need for try and except) try: statement_principal = statement['Principal'] statement_principal_key = "Principal" except KeyError: # In case of NotPrincipal try: statement_principal = statement['NotPrincipal'] statement_principal_key = "NotPrincipal" # In case there is no principal (General AWS Policies do not have explicit mention of principals) except KeyError: statement_principal = set() statement_principal_key = "" if statement_principal: # In case of * as value for Principal if statement_principal == '*' or statement_principal == ['*']: # Sub Key should be AWS (Ref :https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html -> (Everyone (anonymous users)) principal = OrderedDict() principal.__setitem__("AWS", ["*"]) statement_principal = principal for key in statement_principal.keys(): statement_principal[key] = sorted( set( str(statement_principal[key]).replace("'", "").replace( "{", "").replace("}", "").replace("[", "").replace( "]", "").replace(" ", "").split(","))) try: if statement['Condition'] == {}: statement_condition = "" else: # To change it to return to non-string (In case of evaluation) # As of now , its been stringified as it is just used t display as # a property of the statement relation and not actually planned to # evaluate statement_condition = str(json.dumps(statement['Condition'])) except KeyError: statement_condition = "" try: statement_sid = statement['Sid'] except KeyError: statement_sid = "" # Policy Universe's get_actions_from_statement works only in Action and not # NotAction scenario. Hence temporarily converting the Action key to NotAction # and expanding the Action's Wild cards temp = OrderedDict() not_action_flag = 0 for key in statement.keys(): if key == "Action": temp.__setitem__(key, statement_action) elif key == "NotAction": temp.__setitem__("Action", statement_action) not_action_flag = 1 else: temp.__setitem__(key, statement[key]) # statement_aaia_expanded_action variable stores the expanded actions (including inverted NotAction cases). statement_aaia_expanded_action = "" if not_action_flag == 0: statement_aaia_expanded_action = set( expander_minimizer.get_actions_from_statement(temp)) elif not_action_flag == 1: # In case of NotAction all the mentioned actions will be inverted and added to statement_aaia_expanded_action statement_aaia_expanded_action = set( all_permissions.difference( expander_minimizer.get_actions_from_statement(temp))) statement_aaia_expanded_action = sorted( str(statement_aaia_expanded_action).replace("'", "").replace( "{", "").replace("}", "").replace("[", "").replace("]", "").replace(" ", "").split(",")) statement_aaia_expanded_action = str( statement_aaia_expanded_action).replace("'", "").replace( "{", "").replace("}", "").replace("[", "").replace("]", "").replace(" ", "") # ActionKey,ResourceKey,PrincipalKey determines whether it is Action/NotAction , Resource/NotResource and Principal/NotPrincipal respectively in the policy # wheras the Action,Resource,Policy in the below OrderedDict() returns actions,resources,principal respectively as values # Example {"NotAction": "iam:*"} will be returned as # { "ActionKey" : "NotAction, "Action" : "iam:*"} # Hence one has to consider both ActionKey/ResourceKey/PrincipalKey along with Action/Resource/Principal # to evaluate the policy policy_statement_details = OrderedDict() policy_statement_details.__setitem__('Action', statement_action) policy_statement_details.__setitem__('ActionKey', statement_action_key) policy_statement_details.__setitem__('Aaia_ExpandedAction', statement_aaia_expanded_action) policy_statement_details.__setitem__('Effect', statement_effect) policy_statement_details.__setitem__('Resource', statement_resource) policy_statement_details.__setitem__('ResourceKey', statement_resource_key) policy_statement_details.__setitem__('Condition', statement_condition) policy_statement_details.__setitem__('Principal', statement_principal) policy_statement_details.__setitem__('PrincipalKey', statement_principal_key) policy_statement_details.__setitem__('Sid', statement_sid) return policy_statement_details