def __init__(self, statement, resource, resources): self.__statement = copy.deepcopy(statement) self.__resources = resources self.__str__ = lambda: str(statement) assert isinstance(self.__statement, dict) keys = [k for k in self.__statement.keys()] if not ("Effect" in keys and any([k in keys for k in ["Action", "NotAction"]])): console.critical(f"Statement: {self.__statement} " "is missing required key") if "NotPrincipal" in keys: console.warn("'NotPrincipal' support hasn't been implemented." f"Statement: {self.__statement} will be ignored.") elif "Principal" not in keys: self.__statement["Principal"] = {"AWS": [str(resource)]} self._principals = Elements([resource]) if (not any([k in keys for k in ["Resource", "NotResource"]]) and resource is not None): self.__statement["Resource"] = [str(resource)] self._resources = Elements([resource])
def principals(self): principals = Elements() for _, policy in self.documents.items(): results = policy.principals() principals.update(results) return principals
def resolve(self): actions = Elements() for i in range(len(self.statements)): results = self.statements[i].resolve() # [r.set("Statement", i) for r in results] actions.extend(r for r in results if r not in actions) return actions
def __init__(self, session, resources=None, db="default.db"): super().__init__(session=session, default=False) self._db = db if resources is None: self.client = self.session.client("iam") print( "[+] Ingesting AWS::Iam::Users, AWS::Iam::Roles, ", "AWS::Iam::Groups, AWS::Iam::Policies, " "AWS::Iam::InstanceProfiles from IAM") self += self.get_account_authorization_details() self.get_login_profile() self.list_access_keys() elif len(resources) > 0: self += resources # Set IAM entities self.entities = Elements( self.get('AWS::Iam::User') + self.get('AWS::Iam::Role')).get("Resource") # Set Account for a in set([e.account() for e in self.entities.get("Resource")]): self.root = Resource(properties={ "Name": a, "Arn": "arn:aws:iam::%s:root" % a }, labels=["Resource", "AWS::Iam::Root"]) break
def principals(self): principals = Elements() for statement in self.statements: principals.update(statement.principals()) return principals
def actions(self): actions = Elements() for statement in self.statements: actions.update(statement.actions()) return actions
def resolve(self): actions = Elements() for _, policy in self.documents.items(): results = policy.resolve() # [r.set("Policy", name) for r in results] actions.extend([r for r in results if r not in actions]) return actions
def principals(self): principals = Elements() for i in range(len(self.statements)): principals.extend([ p for p in self.statements[i].principals() if p not in principals ]) return principals
def principals(self): principals = Elements() for policy in self.documents.values(): principals.update(policy.principals()) return principals
def __init__(self, statement: dict, resource: Element, resources: Elements): # TODO: policy statements do not appear to strictly adhere to the JSON # format and may include duplicate keys. Duplicate keys will be ignored. self._resources = resources self._statement = statement self._resource = resource self._explicit_principals = None self._explicit_actions = None self._explicit_resources = None self._explicit_conditions = None self._explicit_resource_conditions = {} self._Actions = None try: if not isinstance(statement, dict): raise ValueError self._statement = statement keys = [k.replace("Not", "") for k in self._statement.keys()] if not all([k in keys for k in ["Effect", "Action"]]): raise ValueError("Malformed statement: Missing 'Effect'") if "Resource" not in keys and resource is not None: self._statement["Resource"] = str(self._resource) self._explicit_resources = Elements([self._resource]) self._explicit_resource_conditions = { self._resource.id(): [{}] } elif "Resource" is None: raise ValueError("Malformed statement: Missing 'Resource'") if "Condition" not in keys: self._explicit_conditions = {} else: self._explicit_conditions = self._statement["Condition"] # TODO: Not implemented if "NotPrincipal" in keys: print("[!] 'NotPrincipal' support has not yet been added. " "\n\tThis entire statement will be ingnored (%s)." % self._resource.id()) self._explicit_principals = [] elif "Principal" not in keys: self._explicit_principals = [self._resource] except: raise ValueError("Malformed statement: %s" % self._statement)
def __init__(self, session, resources=None, db="default.db", verbose=False, only_types=[], except_types=[], only_arns=[], except_arns=[]): self._db = db self._verbose = verbose self.account_id = "000000000000" if resources is not None: self += resources return super().__init__(session=session, default=False, verbose=verbose) self.client = self.session.client("iam") self.run = [ r for r in self.run if (len(only_types) == 0 or r in only_types) and r not in except_types ] print("[*] Commencing {resources} ingestion\n".format( resources=', '.join([ r if (i == 0 or i % 3 > 0) else f'\n{" " * 15}{r}' for i, r in enumerate(self.run) ]))) self._print( "[*] Awaiting response to iam:GetAccountAuthorizationDetails " "(this can take a while)") self += self.get_account_authorization_details([], []) if "AWS::Iam::User" in self.run: self.get_login_profile() self.list_access_keys() # Set IAM entities self.entities = Elements( self.get('AWS::Iam::User') + self.get('AWS::Iam::Role')).get("Resource") # Set Account for a in set([e.account() for e in self.entities.get("Resource")]): self.account_id = a break self._print_stats()
def actions(self): actions = Elements() for document in self.documents.values(): actions.update(document.actions()) console.info(f"{self.__class__.__name__} {self.__resource} " f"resolved to {len(actions)} Action(s)") return actions
def resolve(self): if self._Actions is not None: return self._Actions if self._explicit_actions is None: self._resolve_action_statement() if self._explicit_resources is None: self._resolve_resource_statement() if self._explicit_principals is None: self._resolve_principal_statement() actions = Elements() for action in self.actions(): # Rewrite resources = Elements() for affected in ACTIONS[action]["Affects"]: resources.update( Elements(self._explicit_resources.get(affected))) for resource in resources: # Action conditions comprise of resource level permission conditions # variants AND statement conditions condition = self._explicit_resource_conditions[resource.id()] condition = [{ **condition[i], **self._explicit_conditions } for i in range(len(condition))] condition = json.dumps(condition) \ if len(condition[0]) > 0 else "[]" for principal in self._explicit_principals: actions.add( Action(properties={ "Name": action, "Description": ACTIONS[action]["Description"], "Effect": self._statement["Effect"], "Access": ACTIONS[action]["Access"], "Reference": ACTIONS[action]["Reference"], "Condition": condition }, source=principal, target=resource)) # Unset resource level permission conditions for resource in self._explicit_resources: resource.condition = [] self._Actions = actions return self._Actions
def list_functions(self): if 'AWS::Lambda::Function' not in self.types: return functions = Elements() for function in [ f for r in self.console.tasklist( "Adding Functions", iterables=self.client.get_paginator( "list_functions").paginate(), wait="Awaiting response to lambda:ListFunctions", done="Added Functions") for f in r["Functions"] ]: function["Name"] = function["FunctionName"] function["Arn"] = function["FunctionArn"] del function["FunctionName"] del function["FunctionArn"] function = Resource(properties=function, labels=["AWS::Lambda::Function"]) self.add(function)
def list_functions(self): functions = Elements() self._print("[*] Listing functions (this can take a while)") for function in [ f for r in self.client.get_paginator( "list_functions").paginate() for f in r["Functions"] ]: function["Name"] = function["FunctionName"] function["Arn"] = function["FunctionArn"] del function["FunctionName"] del function["FunctionArn"] f = Resource(properties=function, labels=["AWS::Lambda::Function"]) if f not in functions: self._print(f"[*] Adding {f}") functions.add(f) self.update(functions)
def _load_associations(self): if len(self.associates) == 0: return self._print( f"[*] Adding {self.__class__.__name__} associative relationships") edges = Elements() for resource in self.get("Resource"): references = {} label = [l for l in resource.labels() if l != "Resource"][0] # Find references to other resources in the form of a dictionary (refs) self._references(resource.properties(), references) # Create an edge, for all known associations (as defined by self.rels). for rel in [ r for r in self.associates if r[0] == label or r[1] == label ]: i = 1 if label == rel[0] else 0 # Get a list of foreign keys that we must be capable of referencing # in order to create an association fk = [ a for a in re.compile("{([A-Za-z]+)}").findall( RESOURCES.definition(rel[i])) if a not in ["Account", "Region"] ] if not all([k in references.keys() for k in fk]): continue # TODO: Handle Types that make use of more than one # variable identifier if len(fk) != 1: raise NotImplementedError fk = fk[0] for v in list(references[fk]): # Find the first resource matching the reference r = next((r for r in self if re.compile( RESOURCES.definition(rel[i]). format(Account=self.account_id, Region=self.session.region_name, **{ **{ x: list(y)[0] for x, y in references.items( ) if len(y) == 1 }, **{ fk: v } })).match(r.id()) is not None), None) if r is None: # print("Failed to match (%s: %s) against any resources" % (k, v)) # print("Its likely that the resource was missed during ingestion") continue # Delete the properties that are responsible for the edge's existence. properties = self._extract_property_value( resource.properties(), fk) # Even though direction is irrelavent when dealing with Associative # edges, neo4j is directed. We need to ensure the direction is kept # in order to eliminate duplicate edges. (source, target) = (resource, r) if i == 1 else (r, resource) edge = Associative(properties={"Name": "Attached"}, source=source, target=target) opposite_edge = Associative( properties={"Name": "Attached"}, source=target, target=source) if (edge not in self and opposite_edge not in self) and edge not in edges: edges.append(edge) self.extend(edges)
class IAM(Ingestor): run = [ "AWS::Iam::User", "AWS::Iam::Role", "AWS::Iam::Group", "AWS::Iam::Policy", "AWS::Iam::InstanceProfile" ] def __init__(self, session, resources=None, db="default.db", verbose=False, only_types=[], except_types=[], only_arns=[], except_arns=[]): self._db = db self._verbose = verbose self.account_id = "000000000000" if resources is not None: self += resources return super().__init__(session=session, default=False, verbose=verbose) self.client = self.session.client("iam") self.run = [ r for r in self.run if (len(only_types) == 0 or r in only_types) and r not in except_types ] print("[*] Commencing {resources} ingestion\n".format( resources=', '.join([ r if (i == 0 or i % 3 > 0) else f'\n{" " * 15}{r}' for i, r in enumerate(self.run) ]))) self._print( "[*] Awaiting response to iam:GetAccountAuthorizationDetails " "(this can take a while)") self += self.get_account_authorization_details([], []) if "AWS::Iam::User" in self.run: self.get_login_profile() self.list_access_keys() # Set IAM entities self.entities = Elements( self.get('AWS::Iam::User') + self.get('AWS::Iam::Role')).get("Resource") # Set Account for a in set([e.account() for e in self.entities.get("Resource")]): self.account_id = a break self._print_stats() def get_account_authorization_details(self, only_arns, except_arns): elements = Elements() edges = {"Groups": [], "Policies": [], "InstanceProfiles": []} def get_aad_element(label, entry): properties = dict() for pk, pv in sorted(entry.items()): if pk.endswith("PolicyList"): properties["Documents"] = [{ p["PolicyName"]: p["PolicyDocument"] for p in pv }] elif pk == "AssumeRolePolicyDocument": properties["Trusts"] = pv elif pk in [ "GroupList", "InstanceProfileList", "AttachedManagedPolicies" ]: continue elif pk == "PolicyVersionList": properties["Document"] = [{ "DefaultVersion": [p for p in pv if p["IsDefaultVersion"]][0]["Document"] }] else: properties[pk.replace(label, "")] = pv element = Resource(properties=properties, labels=["Resource", f"AWS::Iam::{label}"]) if f"AWS::Iam::Group" in self.run and "GroupList" in entry.keys(): edges["Groups"].extend([(element, g) for g in entry["GroupList"]]) if f"AWS::Iam::InstanceProfile" in self.run \ and "InstanceProfileList" in entry.keys(): edges["InstanceProfiles"].extend([ (get_aad_element("InstanceProfile", ip), element) for ip in entry["InstanceProfileList"] ]) if f"AWS::Iam::Policy" in self.run \ and "AttachedManagedPolicies" in entry.keys(): edges["Policies"].extend([ (element, p["PolicyArn"]) for p in entry["AttachedManagedPolicies"] ]) if (str(f"AWS::Iam::{label}") in self.run and (len(except_arns) == 0 or properties["Arn"] not in except_arns) and (len(only_arns) == 0 or properties["Arn"] in only_arns) and element not in elements): self._print(f"[*] Adding {element}") elements.append(element) return element account_authorization_details = [ aad for aad in self.client.get_paginator( "get_account_authorization_details").paginate() ] account_authorization_details = [ (label.replace("DetailList", "").replace("Policies", "Policy"), entry) for aad in account_authorization_details for (label, v) in aad.items() if isinstance(v, list) for entry in v ] for label, entry in account_authorization_details: get_aad_element(label, entry) # Ensure edge nodes exist for k, v in edges.items(): edges[k] = list( filter(lambda e: e[0] is not None and e[1] is not None, [ e if type(e[1]) == Resource else (e[0], next((t for t in elements if (k == "Groups" and str(t).endswith(str(e[1]))) or str(t) == str(e[1])), None)) for e in v ])) # (:User|Group|Role)-[:TRANSITIVE{Attached}]->(:Policy) for (s, t) in edges["Policies"]: elements.append( Transitive(properties={"Name": "Attached"}, source=s, target=t)) # # (:User)-[:TRANSITIVE{MemberOf}]->(:Group) for (s, t) in edges["Groups"]: elements.append( Transitive(properties={"Name": "MemberOf"}, source=s, target=t)) # (:InstanceProfile)-[:TRANSITIVE{Attached}]->(:Role) for (s, t) in edges["InstanceProfiles"]: del s.properties()["Roles"] elements.append( Transitive(properties={"Name": "Attached"}, source=s, target=t)) return elements def get_login_profile(self): for user in self.get("AWS::Iam::User").get("Resource"): try: login_profile = self.client.get_login_profile( UserName=user.get("Name"))["LoginProfile"] del login_profile["UserName"] user.set("LoginProfile", login_profile) self._print( f"[+] Updated login profile information for {user}") except self.client.exceptions.NoSuchEntityException: pass def list_access_keys(self): for user in self.get("AWS::Iam::User").get("Resource"): try: access_keys = self.client.list_access_keys( UserName=user.get("Name"))["AccessKeyMetadata"] for i in range(len(access_keys)): k = access_keys[i]["AccessKeyId"] del access_keys[i]["AccessKeyId"] del access_keys[i]["UserName"] access_keys[i] = {k: access_keys[i]} user.set("AccessKeys", access_keys) self._print(f"[+] Updated access key information for {user}") except self.client.exceptions.NoSuchEntityException: pass def post(self, skip_actions=False): if not skip_actions: self.resolve() self.transitive() return self.save(self._db) def transitive(self): instances = self.get("AWS::Ec2::Instance").get("Resource") functions = self.get("AWS::Lambda::Function").get("Resource") roles = self.get("AWS::Iam::Role").get("Resource") instance_profiles = self.get("AWS::Iam::InstanceProfile").get( "Resource") # Instance - [TRANSITIVE] -> Iam Instance Profile for instance in instances: if "IamInstanceProfile" not in instance.properties(): continue target = next((ip for ip in instance_profiles if ip.id() == instance.properties()["IamInstanceProfile"]["Arn"]), None) del instance.properties()["IamInstanceProfile"] self.append( Transitive({"Name": "Attached"}, source=instance, target=target)) # Lambda - [TRANSITIVE] -> Role for function in functions: if "Role" not in function.properties(): continue role = next( (r for r in roles if r.id() == function.properties()["Role"]), None) del function.properties()["Role"] self.append( Transitive({"Name": "Attached"}, source=function, target=role)) def resolve(self): IDP = [ "AWS::Iam::User", "AWS::Iam::Role", "AWS::Iam::Group", "AWS::Iam::Policy" ] RBP = {"AWS::S3::Bucket": "Policy", "AWS::Iam::Role": "Trusts"} (principals, actions, trusts) = (Elements(), Elements(), Elements()) resources = self.get("Resource") + self.get("Generic") print("[*] Resolving actions and resources\n") # Resolve actions for resource in self.get("Resource"): self._print(f"[*] Processing {resource}") # Identity Based Policies (Inline and Managed) if resource.labels()[0] in IDP: count = len(actions) actions.extend( IdentityBasedPolicy(resource, resources).resolve()) diff = len(actions) - count if diff > 0: self._print(f"[+] Identity based Policy ({resource}) " "resolved to {diff} action(s)") if resource.labels()[0] in RBP.keys(): # Bucket ACLs if resource.type("AWS::S3::Bucket"): count = len(actions) acl = BucketACL(resource, resources) principals.extend( [p for p in acl.principals() if p not in principals]) actions.extend( [a for a in acl.resolve() if a not in actions]) diff = len(actions) - count if diff > 0: print(f"[+] Bucket ACL ({resource}) " "resolved to {diff} action(s)") # Resource Based Policies rbp = ResourceBasedPolicy(resource, resources, keys=[RBP[resource.labels()[0]]]) if len(rbp.principals()) > 0: count = len(actions) resolved = rbp.resolve() principals.extend([ p for p in rbp.principals() if p not in principals and str(p) != RESOURCES. types["AWS::Account"].format(Account=self.account_id) ]) # TODO: This code should be moved to 'ResourceBasedPolicy' and override resolve(). # For Roles, actions imply a TRUSTS relationship. Only those beginning # with sts:Assume are considered valid. for action in [ a for a in resolved if "AWS::Iam::Role" not in resource.labels() or str(a).startswith("sts:AssumeRole") ]: if action.source().type("AWS::Account") \ and action.source().properties()["Arn"].split(':')[4] == self.account_id: if "AWS::Iam::Role" in resource.labels(): trusts.extend([ Trusts(properties=action.properties(), source=action.target(), target=e) for e in self.entities ]) # This case appears redundant for Buckets else: if not action.source().type("AWS::Domain"): actions.append(action) if "AWS::Iam::Role" in resource.labels(): trusts.append( Trusts(properties=action.properties(), source=action.target(), target=action.source())) diff = len(actions) - count if diff > 0: print(f"[+] Resource based policy ({resource}) " "resolved to {diff} action(s)") principals = [p for p in principals if p not in self] self.extend(principals) self.extend(actions) self.extend(trusts) sys.stdout.write("\033[F\033[K") print( f"[+] Produced {len(principals)} new principals and {len(actions)} actions\n" )
def actions(self): if self._actions is not None: return self._actions (principals, actions, resources, conditions) = (self.principals(), Elements(), self.resources(), self.conditions()) for action in self._get_actions(): action_resources = Elements() # Actions that do not affect specific resource types. if ACTIONS[action]["Affects"] == {}: action_resources.update( Elements(self.__resources.get("CatchAll"))) for affected_type in ACTIONS[action]["Affects"].keys(): # Ignore mutable actions affecting built in policies if (affected_type == "AWS::Iam::Policy" and ACTIONS[action]["Access"] in ["Permissions Management", "Write"]): action_resources.update([ a for a in resources.get(affected_type) if str(a).split(':')[4] != "aws" ]) else: action_resources.update(resources.get(affected_type)) for resource in action_resources: # Action conditions comprise of resource-level conditions and statement conditions resource_conditions = list(conditions[str(resource)] if str( resource) in conditions else [{}]) statement_conditions = dict( self.__statement["Condition"] if "Condition" in self.__statement.keys() else {}) # Add the two together condition = json.dumps([ { **resource_conditions[i], **statement_conditions } for i in range(len(resource_conditions)) ]) if (len(resource_conditions[0]) + len(statement_conditions)) > 0 \ else "[]" # Incorporate all items from ACTIONS.py supplementary = next((ACTIONS[action]["Affects"][r] for r in resource.labels() if r in ACTIONS[action]["Affects"]), {}) for principal in self._principals: actions.add( Action(properties={ "Name": action, "Description": ACTIONS[action]["Description"], "Effect": self.__statement["Effect"], "Access": ACTIONS[action]["Access"], "Reference": ACTIONS[action]["Reference"], "Condition": condition, **supplementary }, source=principal, target=resource)) # Unset resource level permission conditions for resource in self._resources: resource.condition = [] self._actions = actions return self._actions
def get_account_authorization_details(self, only_arns, except_arns): elements = Elements() edges = {"Groups": [], "Policies": [], "InstanceProfiles": []} def get_aad_element(label, entry): properties = dict() for pk, pv in sorted(entry.items()): if pk.endswith("PolicyList"): properties["Documents"] = [{ p["PolicyName"]: p["PolicyDocument"] for p in pv }] elif pk == "AssumeRolePolicyDocument": properties["Trusts"] = pv elif pk in [ "GroupList", "InstanceProfileList", "AttachedManagedPolicies" ]: continue elif pk == "PolicyVersionList": properties["Document"] = [{ "DefaultVersion": [p for p in pv if p["IsDefaultVersion"]][0]["Document"] }] else: properties[pk.replace(label, "")] = pv element = Resource(properties=properties, labels=["Resource", f"AWS::Iam::{label}"]) if f"AWS::Iam::Group" in self.run and "GroupList" in entry.keys(): edges["Groups"].extend([(element, g) for g in entry["GroupList"]]) if f"AWS::Iam::InstanceProfile" in self.run \ and "InstanceProfileList" in entry.keys(): edges["InstanceProfiles"].extend([ (get_aad_element("InstanceProfile", ip), element) for ip in entry["InstanceProfileList"] ]) if f"AWS::Iam::Policy" in self.run \ and "AttachedManagedPolicies" in entry.keys(): edges["Policies"].extend([ (element, p["PolicyArn"]) for p in entry["AttachedManagedPolicies"] ]) if (str(f"AWS::Iam::{label}") in self.run and (len(except_arns) == 0 or properties["Arn"] not in except_arns) and (len(only_arns) == 0 or properties["Arn"] in only_arns) and element not in elements): self._print(f"[*] Adding {element}") elements.append(element) return element account_authorization_details = [ aad for aad in self.client.get_paginator( "get_account_authorization_details").paginate() ] account_authorization_details = [ (label.replace("DetailList", "").replace("Policies", "Policy"), entry) for aad in account_authorization_details for (label, v) in aad.items() if isinstance(v, list) for entry in v ] for label, entry in account_authorization_details: get_aad_element(label, entry) # Ensure edge nodes exist for k, v in edges.items(): edges[k] = list( filter(lambda e: e[0] is not None and e[1] is not None, [ e if type(e[1]) == Resource else (e[0], next((t for t in elements if (k == "Groups" and str(t).endswith(str(e[1]))) or str(t) == str(e[1])), None)) for e in v ])) # (:User|Group|Role)-[:TRANSITIVE{Attached}]->(:Policy) for (s, t) in edges["Policies"]: elements.append( Transitive(properties={"Name": "Attached"}, source=s, target=t)) # # (:User)-[:TRANSITIVE{MemberOf}]->(:Group) for (s, t) in edges["Groups"]: elements.append( Transitive(properties={"Name": "MemberOf"}, source=s, target=t)) # (:InstanceProfile)-[:TRANSITIVE{Attached}]->(:Role) for (s, t) in edges["InstanceProfiles"]: del s.properties()["Roles"] elements.append( Transitive(properties={"Name": "Attached"}, source=s, target=t)) return elements
def _get_resources_and_conditions(self): resources = Elements() conditions = {} all_resources = self.__resources key = list(filter(lambda x: "Resource" in x, self.__statement.keys()))[0] statement = self.__statement[key] \ if isinstance(self.__statement[key], list) \ else [self.__statement[key]] for rlp in set([ r.replace('*', "(.*)") + "$" for r in statement if '*' not in statement and len(all_resources) > 0 ]): # Identify variable resource-level permissions variables = list(re.findall("\$\{[0-9a-zA-Z:]+\}", rlp)) regex = re.compile( reduce(lambda x, y: x.replace(y, "(.*)"), variables, rlp)) # Match resource-level permissions against resource arns results = Elements( filter(lambda r: regex.match(r.id()), all_resources)) # Standard case: add results to result set if len(variables) == 0: for r in results: conditions[r.id()] = [{}] resources.add(r) offset = len([x for x in rlp if x == '(']) + 1 # Handle resource-level permissions for result in [ r for r in results if r.id() not in conditions or conditions[r.id()] != [{}] ]: # TODO: skip resources that incorporate contradictory conditions condition = { "StringEquals": { variables[i]: regex.match(result.id()).group(offset + i) for i in range(len(variables)) } } if result.id() not in conditions: conditions[result.id()] = [] if condition not in conditions[result.id()]: conditions[result.id()].append(condition) if result not in resources: resources.add(result) if '*' in statement: resources = all_resources elif key == "NotResource": resources = [r for r in all_resources if r not in resources] resources = Elements(resources) conditions = { str(r): conditions[r.id()] if r.id() in conditions else [{}] for r in resources } return (resources, conditions)
class Statement: _principals = None _actions = None _resources = None _conditions = None __statement = {} __resources = Elements() def __init__(self, statement, resource, resources): self.__statement = copy.deepcopy(statement) self.__resources = resources self.__str__ = lambda: str(statement) assert isinstance(self.__statement, dict) keys = [k for k in self.__statement.keys()] if not ("Effect" in keys and any([k in keys for k in ["Action", "NotAction"]])): console.critical(f"Statement: {self.__statement} " "is missing required key") if "NotPrincipal" in keys: console.warn("'NotPrincipal' support hasn't been implemented." f"Statement: {self.__statement} will be ignored.") elif "Principal" not in keys: self.__statement["Principal"] = {"AWS": [str(resource)]} self._principals = Elements([resource]) if (not any([k in keys for k in ["Resource", "NotResource"]]) and resource is not None): self.__statement["Resource"] = [str(resource)] self._resources = Elements([resource]) def _get_principals(self): '''https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html''' principals = Elements() key = list(filter(lambda x: "Principal" in x, self.__statement.keys()))[0] statement = self.__statement[key] if isinstance(statement, str) and statement == "*": statement = {"AWS": "*"} assert isinstance(statement, dict) if "AWS" in statement: if not isinstance(statement["AWS"], list): statement["AWS"] = [statement["AWS"]] if '*' in statement["AWS"]: external = External( key="Arn", labels=["AWS::Account"], properties={ "Name": "All AWS Accounts", "Description": "Pseudo-Account representing anyone who possesses an AWS account", "Arn": "arn:aws:iam::{Account}:root" }) principals = Elements([ *self.__resources.get('AWS::Iam::User').get("Resource"), *self.__resources.get('AWS::Iam::Role').get("Resource"), external ]) for principal in [ p for p in statement["AWS"] if '*' not in statement["AWS"] ]: if '*' in principal: continue node = next((a for a in self.__resources.get("Resource") if a.id() == principal), None) # We haven't seen this node before. It may belong to another account, # or it belongs to a service that was not loaded. if node is None: name = principal labels = ["AWS::Account"] if re.compile(f"^{RESOURCES.regex['Account']}$").match( principal) is not None: principal = f"arn:aws:iam::{principal}:root" labels += ["AWS::Account"] elif re.compile( f"^arn:aws:iam::{RESOURCES.regex['Account']}:root$" ).match(principal) is not None: name = str(principal.split(":")[4]) labels += ["AWS::Account"] else: for k, v in RESOURCES.items(): if re.compile(v).match(principal): name = principal.replace('/', ':').split(':')[-1] labels = [k] break node = External( key=str("Arn" if principal. startswith("arn") else "CanonicalUser"), labels=labels, properties={ "Name": str(name), str("Arn" if principal.startswith("arn") else "CanonicalUser"): principal, }) principals.add(node) elif "Service" in statement: services = statement["Service"] if isinstance( statement["Service"], list) else [statement["Service"]] for service in services: if service.lower().endswith("amazonaws.com"): labels = ["AWS::Domain"] else: labels = ["Internet::Domain"] principals.add( External(key="Name", labels=labels, properties={"Name": service})) elif "Federated" in statement: node = None labels = [] statements = statement["Federated"] \ if isinstance(statement["Federated"], list) \ else [statement["Federated"]] for federated in statements: if re.compile(RESOURCES["AWS::Iam::SamlProvider"]).match( federated) is not None: base = Resource if (next( (a for a in self.__resources.get("Resoure") if a.account() == federated.split(':')[4]), False)) else External node = base(key="Arn", labels=["AWS::Iam::SamlProvider"], properties={ "Name": federated.split('/')[-1], "Arn": federated }) elif re.compile( "^(?=.{1,253}\.?$)(?:(?!-|[^.]+_)[A-Za-z0-9-_]{1,63}(?<!-)(?:\.|$)){2,}$" ).match(federated): node = External(key="Name", labels=["Internet::Domain"], properties={"Name": federated}) else: node = External(key="Name", properties={ "Name": federated, }) principals.add(node) # TODO: elif "CanonicalUser" in statement: principals.add( External(key="CanonicalUser", labels=["AWS::Account"], properties={ "Name": statement["CanonicalUser"], "CanonicalUser": statement["CanonicalUser"], })) else: console.warn("Unknown principal: ", statement) return principals def _get_actions(self): actions = set() key = list(filter(lambda x: "Action" in x, self.__statement.keys()))[0] statement = self.__statement[key] if isinstance( self.__statement[key], list) else [self.__statement[key]] for action in [a for a in statement if '*' not in statement]: if '*' in action: actions.update( set( filter( re.compile(action.replace("*", "(.*)")).match, ACTIONS.keys()))) else: actions.update([action] if action in ACTIONS.keys() else []) if '*' in statement and key != 'NotAction': actions = set(ACTIONS.keys()) elif '*' in statement: actions = set() elif key == "NotAction": actions = [action for action in ACTIONS if action not in actions] return sorted(list(actions)) def _get_resources_and_conditions(self): resources = Elements() conditions = {} all_resources = self.__resources key = list(filter(lambda x: "Resource" in x, self.__statement.keys()))[0] statement = self.__statement[key] \ if isinstance(self.__statement[key], list) \ else [self.__statement[key]] for rlp in set([ r.replace('*', "(.*)") + "$" for r in statement if '*' not in statement and len(all_resources) > 0 ]): # Identify variable resource-level permissions variables = list(re.findall("\$\{[0-9a-zA-Z:]+\}", rlp)) regex = re.compile( reduce(lambda x, y: x.replace(y, "(.*)"), variables, rlp)) # Match resource-level permissions against resource arns results = Elements( filter(lambda r: regex.match(r.id()), all_resources)) # Standard case: add results to result set if len(variables) == 0: for r in results: conditions[r.id()] = [{}] resources.add(r) offset = len([x for x in rlp if x == '(']) + 1 # Handle resource-level permissions for result in [ r for r in results if r.id() not in conditions or conditions[r.id()] != [{}] ]: # TODO: skip resources that incorporate contradictory conditions condition = { "StringEquals": { variables[i]: regex.match(result.id()).group(offset + i) for i in range(len(variables)) } } if result.id() not in conditions: conditions[result.id()] = [] if condition not in conditions[result.id()]: conditions[result.id()].append(condition) if result not in resources: resources.add(result) if '*' in statement: resources = all_resources elif key == "NotResource": resources = [r for r in all_resources if r not in resources] resources = Elements(resources) conditions = { str(r): conditions[r.id()] if r.id() in conditions else [{}] for r in resources } return (resources, conditions) def principals(self): if self._principals is None: self._principals = self._get_principals() return self._principals def actions(self): if self._actions is not None: return self._actions (principals, actions, resources, conditions) = (self.principals(), Elements(), self.resources(), self.conditions()) for action in self._get_actions(): action_resources = Elements() # Actions that do not affect specific resource types. if ACTIONS[action]["Affects"] == {}: action_resources.update( Elements(self.__resources.get("CatchAll"))) for affected_type in ACTIONS[action]["Affects"].keys(): # Ignore mutable actions affecting built in policies if (affected_type == "AWS::Iam::Policy" and ACTIONS[action]["Access"] in ["Permissions Management", "Write"]): action_resources.update([ a for a in resources.get(affected_type) if str(a).split(':')[4] != "aws" ]) else: action_resources.update(resources.get(affected_type)) for resource in action_resources: # Action conditions comprise of resource-level conditions and statement conditions resource_conditions = list(conditions[str(resource)] if str( resource) in conditions else [{}]) statement_conditions = dict( self.__statement["Condition"] if "Condition" in self.__statement.keys() else {}) # Add the two together condition = json.dumps([ { **resource_conditions[i], **statement_conditions } for i in range(len(resource_conditions)) ]) if (len(resource_conditions[0]) + len(statement_conditions)) > 0 \ else "[]" # Incorporate all items from ACTIONS.py supplementary = next((ACTIONS[action]["Affects"][r] for r in resource.labels() if r in ACTIONS[action]["Affects"]), {}) for principal in self._principals: actions.add( Action(properties={ "Name": action, "Description": ACTIONS[action]["Description"], "Effect": self.__statement["Effect"], "Access": ACTIONS[action]["Access"], "Reference": ACTIONS[action]["Reference"], "Condition": condition, **supplementary }, source=principal, target=resource)) # Unset resource level permission conditions for resource in self._resources: resource.condition = [] self._actions = actions return self._actions def resources(self): if self._resources is None: (self._resources, self._conditions) = self._get_resources_and_conditions() return self._resources def conditions(self): if self._conditions is None: (self._resources, self._conditions) = self._get_resources_and_conditions() return self._conditions
def _resolve_principal_statement(self): principals = Elements() key = list(filter(lambda x: "Principal" in x, self._statement.keys()))[0] statement = self._statement[key] if isinstance(statement, str) and statement == "*": statement = {"AWS": "*"} if not isinstance(statement, dict): raise ValueError if "AWS" in statement: if not isinstance(statement["AWS"], list): statement["AWS"] = [statement["AWS"]] if '*' in statement["AWS"]: principals = self._resources.get('AWS::Iam::User').get( "Resource") + self._resources.get('AWS::Iam::Role').get( "Resource") + [ External(key="Arn", labels=["AWS::Account"], properties={ "Name": "All AWS Accounts", "Arn": "arn:aws:iam::{Account}:root" }) ] for principal in [ p for p in statement["AWS"] if '*' not in statement["AWS"] ]: if '*' in principal: continue node = next( (a for a in self._resources if a.id() == principal), None) # We haven't seen this node before. It may belong to another account, # or it belongs to a service that was not loaded. if node is None: name = principal labels = [] if re.compile("^%s$" % RESOURCES.regex["Account"]).match( principal) is not None: labels += ["AWS::Account"] principal = "arn:aws:iam::{Account}:root".format( Account=principal) elif re.compile( "^%s$" % "arn:aws:iam::{Account}:root".format( Account=RESOURCES.regex["Account"])).match( principal) is not None: name = str(principal.split(":")[4]) labels += ["AWS::Account"] else: for k, v in RESOURCES.items(): if re.compile(v).match(principal): name = principal.replace('/', ':').split(':')[-1] labels = [k] break node = External(key="Arn", labels=labels, properties={ "Name": str(name), "Arn": principal }) principals.add(node) elif "Service" in statement: services = statement["Service"] if isinstance( statement["Service"], list) else [statement["Service"]] for service in services: if service.endswith("amazonaws.com"): labels = ["AWS::Domain"] else: labels = ["Internet::Domain"] principals.add( External(labels=labels, properties={"Name": service})) elif "Federated" in statement: node = None labels = [] if re.compile(RESOURCES["AWS::Iam::SamlProvider"]).match( statement["Federated"]) is not None: base = Resource if (next( (a for a in self._resources if a.id().split(':')[4] == statement["Federated"].split( ':')[4]), False)) else External node = base(key="Arn", labels=["AWS::Iam::SamlProvider"], properties={ "Name": statement["Federated"].split('/')[-1], "Arn": statement["Federated"] }) elif re.compile( "^(?=.{1,253}\.?$)(?:(?!-|[^.]+_)[A-Za-z0-9-_]{1,63}(?<!-)(?:\.|$)){2,}$" ).match(statement["Federated"]): node = External(labels=["Internet::Domain"], properties={"Name": statement["Federated"]}) else: node = External(properties={ "Name": statement["Federated"], }) principals.add(node) # TODO: elif "CanonicalUser" in statement: principals.add( External(labels=["AWS::Account"], properties={ "Name": statement["CanonicalUser"], "CanonicalUser": statement["CanonicalUser"], "Arn": "" })) else: print("Unknown pricipal: ", statement) self._explicit_principals = principals
def resolve(self): IDP = [ "AWS::Iam::User", "AWS::Iam::Role", "AWS::Iam::Group", "AWS::Iam::Policy" ] RBP = {"AWS::S3::Bucket": "Policy", "AWS::Iam::Role": "Trusts"} (principals, actions, trusts) = (Elements(), Elements(), Elements()) resources = self.get("Resource") + self.get("Generic") print("[*] Resolving actions and resources\n") # Resolve actions for resource in self.get("Resource"): self._print(f"[*] Processing {resource}") # Identity Based Policies (Inline and Managed) if resource.labels()[0] in IDP: count = len(actions) actions.extend( IdentityBasedPolicy(resource, resources).resolve()) diff = len(actions) - count if diff > 0: self._print(f"[+] Identity based Policy ({resource}) " "resolved to {diff} action(s)") if resource.labels()[0] in RBP.keys(): # Bucket ACLs if resource.type("AWS::S3::Bucket"): count = len(actions) acl = BucketACL(resource, resources) principals.extend( [p for p in acl.principals() if p not in principals]) actions.extend( [a for a in acl.resolve() if a not in actions]) diff = len(actions) - count if diff > 0: print(f"[+] Bucket ACL ({resource}) " "resolved to {diff} action(s)") # Resource Based Policies rbp = ResourceBasedPolicy(resource, resources, keys=[RBP[resource.labels()[0]]]) if len(rbp.principals()) > 0: count = len(actions) resolved = rbp.resolve() principals.extend([ p for p in rbp.principals() if p not in principals and str(p) != RESOURCES. types["AWS::Account"].format(Account=self.account_id) ]) # TODO: This code should be moved to 'ResourceBasedPolicy' and override resolve(). # For Roles, actions imply a TRUSTS relationship. Only those beginning # with sts:Assume are considered valid. for action in [ a for a in resolved if "AWS::Iam::Role" not in resource.labels() or str(a).startswith("sts:AssumeRole") ]: if action.source().type("AWS::Account") \ and action.source().properties()["Arn"].split(':')[4] == self.account_id: if "AWS::Iam::Role" in resource.labels(): trusts.extend([ Trusts(properties=action.properties(), source=action.target(), target=e) for e in self.entities ]) # This case appears redundant for Buckets else: if not action.source().type("AWS::Domain"): actions.append(action) if "AWS::Iam::Role" in resource.labels(): trusts.append( Trusts(properties=action.properties(), source=action.target(), target=action.source())) diff = len(actions) - count if diff > 0: print(f"[+] Resource based policy ({resource}) " "resolved to {diff} action(s)") principals = [p for p in principals if p not in self] self.extend(principals) self.extend(actions) self.extend(trusts) sys.stdout.write("\033[F\033[K") print( f"[+] Produced {len(principals)} new principals and {len(actions)} actions\n" )
def resolve(self): actions = Elements() for _, policy in self.documents.items(): actions.update(policy.resolve()) return actions
def resolve(self): actions = Elements() for i in range(len(self.statements)): actions.update(self.statements[i].resolve()) return actions
def _get_principals(self): '''https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html''' principals = Elements() key = list(filter(lambda x: "Principal" in x, self.__statement.keys()))[0] statement = self.__statement[key] if isinstance(statement, str) and statement == "*": statement = {"AWS": "*"} assert isinstance(statement, dict) if "AWS" in statement: if not isinstance(statement["AWS"], list): statement["AWS"] = [statement["AWS"]] if '*' in statement["AWS"]: external = External( key="Arn", labels=["AWS::Account"], properties={ "Name": "All AWS Accounts", "Description": "Pseudo-Account representing anyone who possesses an AWS account", "Arn": "arn:aws:iam::{Account}:root" }) principals = Elements([ *self.__resources.get('AWS::Iam::User').get("Resource"), *self.__resources.get('AWS::Iam::Role').get("Resource"), external ]) for principal in [ p for p in statement["AWS"] if '*' not in statement["AWS"] ]: if '*' in principal: continue node = next((a for a in self.__resources.get("Resource") if a.id() == principal), None) # We haven't seen this node before. It may belong to another account, # or it belongs to a service that was not loaded. if node is None: name = principal labels = ["AWS::Account"] if re.compile(f"^{RESOURCES.regex['Account']}$").match( principal) is not None: principal = f"arn:aws:iam::{principal}:root" labels += ["AWS::Account"] elif re.compile( f"^arn:aws:iam::{RESOURCES.regex['Account']}:root$" ).match(principal) is not None: name = str(principal.split(":")[4]) labels += ["AWS::Account"] else: for k, v in RESOURCES.items(): if re.compile(v).match(principal): name = principal.replace('/', ':').split(':')[-1] labels = [k] break node = External( key=str("Arn" if principal. startswith("arn") else "CanonicalUser"), labels=labels, properties={ "Name": str(name), str("Arn" if principal.startswith("arn") else "CanonicalUser"): principal, }) principals.add(node) elif "Service" in statement: services = statement["Service"] if isinstance( statement["Service"], list) else [statement["Service"]] for service in services: if service.lower().endswith("amazonaws.com"): labels = ["AWS::Domain"] else: labels = ["Internet::Domain"] principals.add( External(key="Name", labels=labels, properties={"Name": service})) elif "Federated" in statement: node = None labels = [] statements = statement["Federated"] \ if isinstance(statement["Federated"], list) \ else [statement["Federated"]] for federated in statements: if re.compile(RESOURCES["AWS::Iam::SamlProvider"]).match( federated) is not None: base = Resource if (next( (a for a in self.__resources.get("Resoure") if a.account() == federated.split(':')[4]), False)) else External node = base(key="Arn", labels=["AWS::Iam::SamlProvider"], properties={ "Name": federated.split('/')[-1], "Arn": federated }) elif re.compile( "^(?=.{1,253}\.?$)(?:(?!-|[^.]+_)[A-Za-z0-9-_]{1,63}(?<!-)(?:\.|$)){2,}$" ).match(federated): node = External(key="Name", labels=["Internet::Domain"], properties={"Name": federated}) else: node = External(key="Name", properties={ "Name": federated, }) principals.add(node) # TODO: elif "CanonicalUser" in statement: principals.add( External(key="CanonicalUser", labels=["AWS::Account"], properties={ "Name": statement["CanonicalUser"], "CanonicalUser": statement["CanonicalUser"], })) else: console.warn("Unknown principal: ", statement) return principals
def handle_ingest(args): """ awspx ingest """ resources = Elements() account = "000000000000" session = None graph = None # Check to see if environment variables are being used for credentials. if args.env: session = boto3.session.Session(region_name=args.region) # Use existing profile elif args.profile in CREDENTIALS.sections(): session = boto3.session.Session(region_name=args.region, profile_name=args.profile) # Use instance profile elif args.profile == "default": try: provider = InstanceMetadataProvider( iam_role_fetcher=InstanceMetadataFetcher()) creds = provider.load() session = boto3.session.Session(region_name=args.region, aws_access_key_id=creds.access_key, aws_secret_access_key=creds.secret_key, aws_session_token=creds.token) except: pass # Create new profile if not session: if input(f"[-] Would you like to create the profile '{args.profile}'? (y/n) ").upper() == "Y": args.create_profile = args.profile handle_profile(args) session = boto3.session.Session(region_name=args.region, profile_name=args.profile) else: sys.exit(1) try: identity = session.client('sts').get_caller_identity() account = identity["Account"] print(f"[+] Profile: {args.profile} (identity: {identity['Arn']})") except: print("[-] Request to establish identity (sts:GetCallerIdentity) failed.") sys.exit(1) print(f"[+] Services: {', '.join([s.__name__ for s in args.services])}") print(f"[+] Database: {args.database}") print(f"[+] Region: {args.region}") if args.role_to_assume: try: response = session.client('sts').assume_role( RoleArn=args.role_to_assume, RoleSessionName=f"awspx", DurationSeconds=args.role_to_assume_duration) except ClientError as e: print("\n" + str(e)) if "MaxSessionDuration" in e.response["Error"]["Message"]: print("\nTry reducing the session duration using " "'--assume-role-duration'.") sys.exit(1) if response: print(f"[+] Assumed role: {args.role_to_assume}") session = boto3.session.Session( aws_access_key_id=response["Credentials"]["AccessKeyId"], aws_secret_access_key=response["Credentials"]["SecretAccessKey"], aws_session_token=response["Credentials"]["SessionToken"], region_name=args.region) try: identity = session.client('sts').get_caller_identity() account = identity["Account"] print(f"[+] Running as {identity['Arn']}.") print(f"[+] Region set to {args.region}.") except: print("[-] Request to establish identity (sts:GetCallerIdentity) failed.") print() if session is None: sys.exit(1) # Run IAM first to try acquire an account number if IAM in args.services: graph = IAM(session, db=args.database, verbose=args.verbose, quick=args.quick, only_types=args.only_types, skip_types=args.skip_types, only_arns=args.only_arns, skip_arns=args.skip_arns) account = graph.account_id for service in [s for s in args.services if s != IAM]: resources += service(session, account=account, verbose=args.verbose, quick=args.quick, only_types=args.only_types, skip_types=args.skip_types, only_arns=args.only_arns, skip_arns=args.skip_arns) if graph is None: graph = IAM(session, verbose=args.verbose, quick=args.quick, db=args.database, resources=resources) else: graph.update(resources) args.load_zip = graph.post(skip_all_actions=args.skip_all_actions) handle_db(args) if not (args.skip_all_attacks or args.skip_all_actions): handle_attacks(args)
class Statement: def __init__(self, statement: dict, resource: Element, resources: Elements): # TODO: policy statements do not appear to strictly adhere to the JSON # format and may include duplicate keys. Duplicate keys will be ignored. self._resources = resources self._statement = statement self._resource = resource self._explicit_principals = None self._explicit_actions = None self._explicit_resources = None self._explicit_conditions = None self._explicit_resource_conditions = {} self._Actions = None try: if not isinstance(statement, dict): raise ValueError self._statement = statement keys = [k.replace("Not", "") for k in self._statement.keys()] if not all([k in keys for k in ["Effect", "Action"]]): raise ValueError("Malformed statement: Missing 'Effect'") if "Resource" not in keys and resource is not None: self._statement["Resource"] = str(self._resource) self._explicit_resources = Elements([self._resource]) self._explicit_resource_conditions = { self._resource.id(): [{}] } elif "Resource" is None: raise ValueError("Malformed statement: Missing 'Resource'") if "Condition" not in keys: self._explicit_conditions = {} else: self._explicit_conditions = self._statement["Condition"] # TODO: Not implemented if "NotPrincipal" in keys: print("[!] 'NotPrincipal' support has not yet been added. " "\n\tThis entire statement will be ingnored (%s)." % self._resource.id()) self._explicit_principals = [] elif "Principal" not in keys: self._explicit_principals = [self._resource] except: raise ValueError("Malformed statement: %s" % self._statement) '''https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html''' def _resolve_principal_statement(self): principals = Elements() key = list(filter(lambda x: "Principal" in x, self._statement.keys()))[0] statement = self._statement[key] if isinstance(statement, str) and statement == "*": statement = {"AWS": "*"} if not isinstance(statement, dict): raise ValueError if "AWS" in statement: if not isinstance(statement["AWS"], list): statement["AWS"] = [statement["AWS"]] if '*' in statement["AWS"]: principals = self._resources.get('AWS::Iam::User').get( "Resource") + self._resources.get('AWS::Iam::Role').get( "Resource") + [ External(key="Arn", labels=["AWS::Account"], properties={ "Name": "All AWS Accounts", "Arn": "arn:aws:iam::{Account}:root" }) ] for principal in [ p for p in statement["AWS"] if '*' not in statement["AWS"] ]: if '*' in principal: continue node = next( (a for a in self._resources if a.id() == principal), None) # We haven't seen this node before. It may belong to another account, # or it belongs to a service that was not loaded. if node is None: name = principal labels = [] if re.compile("^%s$" % RESOURCES.regex["Account"]).match( principal) is not None: labels += ["AWS::Account"] principal = "arn:aws:iam::{Account}:root".format( Account=principal) elif re.compile( "^%s$" % "arn:aws:iam::{Account}:root".format( Account=RESOURCES.regex["Account"])).match( principal) is not None: name = str(principal.split(":")[4]) labels += ["AWS::Account"] else: for k, v in RESOURCES.items(): if re.compile(v).match(principal): name = principal.replace('/', ':').split(':')[-1] labels = [k] break node = External(key="Arn", labels=labels, properties={ "Name": str(name), "Arn": principal }) principals.add(node) elif "Service" in statement: services = statement["Service"] if isinstance( statement["Service"], list) else [statement["Service"]] for service in services: if service.endswith("amazonaws.com"): labels = ["AWS::Domain"] else: labels = ["Internet::Domain"] principals.add( External(labels=labels, properties={"Name": service})) elif "Federated" in statement: node = None labels = [] if re.compile(RESOURCES["AWS::Iam::SamlProvider"]).match( statement["Federated"]) is not None: base = Resource if (next( (a for a in self._resources if a.id().split(':')[4] == statement["Federated"].split( ':')[4]), False)) else External node = base(key="Arn", labels=["AWS::Iam::SamlProvider"], properties={ "Name": statement["Federated"].split('/')[-1], "Arn": statement["Federated"] }) elif re.compile( "^(?=.{1,253}\.?$)(?:(?!-|[^.]+_)[A-Za-z0-9-_]{1,63}(?<!-)(?:\.|$)){2,}$" ).match(statement["Federated"]): node = External(labels=["Internet::Domain"], properties={"Name": statement["Federated"]}) else: node = External(properties={ "Name": statement["Federated"], }) principals.add(node) # TODO: elif "CanonicalUser" in statement: principals.add( External(labels=["AWS::Account"], properties={ "Name": statement["CanonicalUser"], "CanonicalUser": statement["CanonicalUser"], "Arn": "" })) else: print("Unknown pricipal: ", statement) self._explicit_principals = principals def _resolve_action_statement(self): actions = set() key = list(filter(lambda x: "Action" in x, self._statement.keys()))[0] statement = self._statement[key] \ if isinstance(self._statement[key], list) \ else [self._statement[key]] for action in [a for a in statement if '*' not in statement]: if '*' in action: actions.update( set( filter( re.compile(action.replace("*", "(.*)")).match, ACTIONS.keys()))) else: actions.update([action] if action in ACTIONS.keys() else []) if '*' in statement and key != 'NotAction': actions = set(ACTIONS.keys()) elif '*' in statement: actions = set() elif key == "NotAction": actions = [action for action in ACTIONS if action not in actions] self._explicit_actions = sorted(list(actions)) def _resolve_resource_statement(self): resources = Elements() conditions = {} key = list(filter(lambda x: "Resource" in x, self._statement.keys()))[0] statement = self._statement[key] \ if isinstance(self._statement[key], list) \ else [self._statement[key]] for resource in set([ r.replace('*', "(.*)") + "$" for r in statement if '*' not in statement and len(self._resources) > 0 ]): # Identify variable resource-level permissions variables = list(re.findall("\$\{[0-9a-zA-Z:]+\}", resource)) regex = re.compile( reduce(lambda x, y: x.replace(y, "(.*)"), variables, resource)) # Match resource-level permissions against resource arns results = Elements( filter(lambda r: regex.match(r.id()), self._resources)) # Standard case: add results to result set if len(variables) == 0: for r in results: conditions[r.id()] = [{}] resources.add(r) offset = len([x for x in resource if x == '(']) + 1 # Handle resource-level permissions for result in [ r for r in results if r.id() not in conditions or conditions[r.id()] != [{}] ]: # TODO: skip resources that incorporate contradictory conditions condition = { "StringEquals": { variables[i]: regex.match(result.id()).group(offset + i) for i in range(len(variables)) } } if result.id() not in conditions: conditions[result.id()] = [] if condition not in conditions[result.id()]: conditions[result.id()].append(condition) if result not in resources: resources.add(result) if '*' in statement: resources = self._resources elif key == "NotResource": resources = [r for r in self._resources if r not in resources] self._explicit_resource_conditions = { r.id(): conditions[r.id()] if r.id() in conditions else [{}] for r in resources } self._explicit_resources = Elements(resources) def __str__(self): return str(self._statement) def principals(self): if self._explicit_principals is None: self._resolve_principal_statement() return self._explicit_principals def actions(self): if self._explicit_actions is None: self._resolve_action_statement() return self._explicit_actions def resources(self): if self._explicit_resources is None: self._resolve_resource_statement() return [str(r) for r in self._explicit_resources] def resolve(self): if self._Actions is not None: return self._Actions if self._explicit_actions is None: self._resolve_action_statement() if self._explicit_resources is None: self._resolve_resource_statement() if self._explicit_principals is None: self._resolve_principal_statement() actions = Elements() for action in self.actions(): # Rewrite resources = Elements() for affected in ACTIONS[action]["Affects"]: resources.update( Elements(self._explicit_resources.get(affected))) for resource in resources: # Action conditions comprise of resource level permission conditions # variants AND statement conditions condition = self._explicit_resource_conditions[resource.id()] condition = [{ **condition[i], **self._explicit_conditions } for i in range(len(condition))] condition = json.dumps(condition) \ if len(condition[0]) > 0 else "[]" for principal in self._explicit_principals: actions.add( Action(properties={ "Name": action, "Description": ACTIONS[action]["Description"], "Effect": self._statement["Effect"], "Access": ACTIONS[action]["Access"], "Reference": ACTIONS[action]["Reference"], "Condition": condition }, source=principal, target=resource)) # Unset resource level permission conditions for resource in self._explicit_resources: resource.condition = [] self._Actions = actions return self._Actions
def handle_ingest(args): """ awspx ingest """ resources = Elements() account = "000000000000" session = None graph = None if args.profile not in CREDENTIALS.sections(): if input(f"[-] The profile '{args.profile}' was not found. " "Would you like to create it? (y/n) ").upper() == "Y": args.create_profile = args.profile handle_profile(args) else: return try: session = boto3.session.Session(profile_name=args.profile, region_name=args.region) identity = session.client('sts').get_caller_identity() account = identity["Account"] print(f"[+] User set to: {identity['Arn']}") except: print( "[-] Request to establish identity (sts:GetCallerIdentity) failed." ) print(f"[+] Region set to: {args.region}") print(f"[+] Database set to: {args.database}") if args.role_to_assume: response = session.client('sts').assume_role( RoleArn=args.role_to_assume, RoleSessionName=f"awspx", DurationSeconds=7200) if response: print(f"[+] Assumed role: {args.role_to_assume}") session = boto3.session.Session( aws_access_key_id=response["Credentials"]["AccessKeyId"], aws_secret_access_key=response["Credentials"] ["SecretAccessKey"], aws_session_token=response["Credentials"]["SessionToken"], region_name=args.region) try: identity = session.client('sts').get_caller_identity() account = identity["Account"] print(f"[+] Running as {identity['Arn']}.") print(f"[+] Region set to {args.region}.") except: print( "[-] Request to establish identity (sts:GetCallerIdentity) failed." ) print() if session is None: sys.exit(1) # Run IAM first to try acquire an account number if IAM in args.services: graph = IAM(session, db=args.database, verbose=args.verbose, only_types=args.only_types, except_types=args.except_types, only_arns=args.only_arns, except_arns=args.except_arns) account = graph.account_id for service in [s for s in args.services if s != IAM]: resources += service(session, account=account, verbose=args.verbose, only_types=args.only_types, except_types=args.except_types, only_arns=args.only_arns, except_arns=args.except_arns) if graph is None: graph = IAM(session, verbose=args.verbose, db=args.database, resources=resources) else: graph.update(resources) args.load_zip = graph.post(skip_actions=args.skip_actions) handle_db(args) if not (args.skip_attacks or args.skip_actions): handle_attacks(args)
def principals(self): principals = Elements() for i in range(len(self.statements)): principals.update(self.statements[i].principals()) return principals