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): 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 _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_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 _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 get_account_authorization_details(self, only_arns, except_arns): elements = Elements() edges = {"Groups": [], "Policies": [], "InstanceProfiles": []} self._print( "[*] Awaiting response to iam:GetAccountAuthorizationDetails " "(this can take a while)") 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.add(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.add( Transitive(properties={"Name": "Attached"}, source=s, target=t)) # # (:User)-[:TRANSITIVE{MemberOf}]->(:Group) for (s, t) in edges["Groups"]: elements.add( Transitive(properties={"Name": "MemberOf"}, source=s, target=t)) # (:InstanceProfile)-[:TRANSITIVE{Attached}]->(:Role) for (s, t) in edges["InstanceProfiles"]: del s.properties()["Roles"] elements.add( Transitive(properties={"Name": "Attached"}, source=s, target=t)) self.update(elements)
def _load_associations(self): if len(self.associates) == 0: return edges = Elements() self._print(f"[*] Adding {self.__class__.__name__} " "associative relationships") 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(str(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.add(edge) self.update(edges)
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(): resources = Elements() # Actions that do not affect specific resource types. if ACTIONS[action]["Affects"] == {}: resources.update( Elements(self._explicit_resources.get("CatchAll"))) for affected_type in ACTIONS[action]["Affects"].keys(): affected = self._explicit_resources.get(affected_type) # Ignore mutable actions affecting built in policies if affected_type == "AWS::Iam::Policy" \ and ACTIONS[action]["Access"] in ["Permissions Management", "Write"]: affected = [ a for a in affected if str(a).split(':')[4] != "aws" ] resources.update(Elements(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 "[]" supplementary = next((ACTIONS[action]["Affects"][r] for r in resource.labels() if r in ACTIONS[action]["Affects"]), {}) 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, **supplementary }, source=principal, target=resource)) # Unset resource level permission conditions for resource in self._explicit_resources: resource.condition = [] self._Actions = actions return self._Actions