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.extend(Elements( [r for r in self._explicit_resources.get(affected) if r not in resources])) 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.append(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 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 _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)
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.append(node) elif "Service" in statement: pass # 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.append(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.append(node) # TODO: elif "CanonicalUser" in statement: principals.append( External(labels=["AWS::Account"], properties={ "Name": statement["CanonicalUser"], "CanonicalUser": statement["CanonicalUser"], "Arn": "" })) else: print("Unknown pricipal: ", statement) self._explicit_principals = principals
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()] = [{}] if r not in resources: resources.append(r) continue 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.append(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(sorted(resources))
def get_account_authorization_details(self): 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", "AWS::Iam::%s" % label]) if "GroupList" in entry.keys(): edges["Groups"].extend([(element, g) for g in entry["GroupList"]]) if "InstanceProfileList" in entry.keys(): edges["InstanceProfiles"].extend([ (get_aad_element("InstanceProfile", ip), element) for ip in entry["InstanceProfileList"] ]) if "AttachedManagedPolicies" in entry.keys(): edges["Policies"].extend([ (element, p["PolicyArn"]) for p in entry["AttachedManagedPolicies"] ]) if element not in elements: 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) # User|Group|Role - Attached -> Policy for (s, t) in edges["Policies"]: t = next(entry for entry in elements if entry.id() == t) elements.append( Transitive(properties={"Name": "Attached"}, source=s, target=t)) # User - [MemberOf] -> Group for (s, t) in edges["Groups"]: t = next(entry for entry in elements.get("AWS::Iam::Group") if str(entry).endswith(t)) elements.append( Transitive(properties={"Name": "MemberOf"}, source=s, target=t)) # InstanceProfile - [Attached] -> Role for (s, t) in edges["InstanceProfiles"]: del s.properties()["Roles"] elements.append( Transitive(properties={"Name": "Attached"}, source=s, target=t)) return elements