Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
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"
        )
Ejemplo n.º 4
0
class IAM(Ingestor):
    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 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

    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)

            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)

            except self.client.exceptions.NoSuchEntityException:
                pass

    def post(self):
        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")

        # Resolve actions
        for resource in self.get("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:
                    print(f"[+] Identity based Policy for `{resource}` "
                          f"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 for `{resource}` "
                              f"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.root.account())
                    ])

                    # TODO: This code should be moved to 'ResourceBasedPolicy' and override resolve().

                    # For Roles, actions imply a TRUSTS relationship. For both (ACTION and TRUSTS, only actions 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.root.account():

                            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:
                            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 for `{resource}` "
                              f"resolved to {diff} action(s)")

        self.extend([p for p in principals if p not in self])
        self.extend([a for a in actions if a not in self])
        self.extend(trusts)