cartography 0.110.0rc2__py3-none-any.whl → 0.111.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cartography might be problematic. Click here for more details.
- cartography/_version.py +16 -3
- cartography/cli.py +46 -0
- cartography/config.py +16 -0
- cartography/data/indexes.cypher +0 -2
- cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
- cartography/graph/querybuilder.py +70 -0
- cartography/intel/aws/apigateway.py +113 -4
- cartography/intel/aws/ec2/vpc.py +140 -124
- cartography/intel/aws/eventbridge.py +73 -0
- cartography/intel/github/repos.py +28 -12
- cartography/intel/github/util.py +12 -0
- cartography/intel/keycloak/__init__.py +153 -0
- cartography/intel/keycloak/authenticationexecutions.py +322 -0
- cartography/intel/keycloak/authenticationflows.py +77 -0
- cartography/intel/keycloak/clients.py +187 -0
- cartography/intel/keycloak/groups.py +126 -0
- cartography/intel/keycloak/identityproviders.py +94 -0
- cartography/intel/keycloak/organizations.py +163 -0
- cartography/intel/keycloak/realms.py +61 -0
- cartography/intel/keycloak/roles.py +202 -0
- cartography/intel/keycloak/scopes.py +73 -0
- cartography/intel/keycloak/users.py +70 -0
- cartography/intel/keycloak/util.py +47 -0
- cartography/models/aws/apigateway/apigatewaydeployment.py +74 -0
- cartography/models/aws/ec2/vpc.py +46 -0
- cartography/models/aws/ec2/vpc_cidr.py +102 -0
- cartography/models/aws/eventbridge/target.py +71 -0
- cartography/models/keycloak/__init__.py +0 -0
- cartography/models/keycloak/authenticationexecution.py +160 -0
- cartography/models/keycloak/authenticationflow.py +54 -0
- cartography/models/keycloak/client.py +177 -0
- cartography/models/keycloak/group.py +101 -0
- cartography/models/keycloak/identityprovider.py +89 -0
- cartography/models/keycloak/organization.py +116 -0
- cartography/models/keycloak/organizationdomain.py +73 -0
- cartography/models/keycloak/realm.py +173 -0
- cartography/models/keycloak/role.py +126 -0
- cartography/models/keycloak/scope.py +73 -0
- cartography/models/keycloak/user.py +51 -0
- cartography/models/tailscale/device.py +1 -0
- cartography/sync.py +2 -0
- cartography/util.py +8 -0
- {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/METADATA +2 -1
- {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/RECORD +53 -25
- cartography/data/jobs/cleanup/aws_import_vpc_cleanup.json +0 -23
- /cartography/models/aws/{__init__.py → apigateway/__init__.py} +0 -0
- /cartography/models/aws/{apigateway.py → apigateway/apigateway.py} +0 -0
- /cartography/models/aws/{apigatewaycertificate.py → apigateway/apigatewaycertificate.py} +0 -0
- /cartography/models/aws/{apigatewayresource.py → apigateway/apigatewayresource.py} +0 -0
- /cartography/models/aws/{apigatewaystage.py → apigateway/apigatewaystage.py} +0 -0
- {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/WHEEL +0 -0
- {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from cartography.client.core.tx import load
|
|
8
|
+
from cartography.graph.job import GraphJob
|
|
9
|
+
from cartography.intel.keycloak.util import get_paginated
|
|
10
|
+
from cartography.models.keycloak.identityprovider import KeycloakIdentityProviderSchema
|
|
11
|
+
from cartography.util import timeit
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
15
|
+
_TIMEOUT = (60, 60)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@timeit
|
|
19
|
+
def sync(
|
|
20
|
+
neo4j_session: neo4j.Session,
|
|
21
|
+
api_session: requests.Session,
|
|
22
|
+
base_url: str,
|
|
23
|
+
common_job_parameters: dict[str, Any],
|
|
24
|
+
) -> None:
|
|
25
|
+
identityproviders = get(
|
|
26
|
+
api_session,
|
|
27
|
+
base_url,
|
|
28
|
+
common_job_parameters["REALM"],
|
|
29
|
+
)
|
|
30
|
+
idps_transformed = transform(identityproviders)
|
|
31
|
+
load_identityproviders(
|
|
32
|
+
neo4j_session,
|
|
33
|
+
idps_transformed,
|
|
34
|
+
common_job_parameters["REALM"],
|
|
35
|
+
common_job_parameters["UPDATE_TAG"],
|
|
36
|
+
)
|
|
37
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@timeit
|
|
41
|
+
def get(
|
|
42
|
+
api_session: requests.Session,
|
|
43
|
+
base_url: str,
|
|
44
|
+
realm: str,
|
|
45
|
+
) -> list[dict[str, Any]]:
|
|
46
|
+
result: list[dict[str, Any]] = []
|
|
47
|
+
url = f"{base_url}/admin/realms/{realm}/identity-provider/instances"
|
|
48
|
+
for idp in get_paginated(api_session, url, params={"briefRepresentation": False}):
|
|
49
|
+
# Get members
|
|
50
|
+
members_url = f"{base_url}/admin/realms/{realm}/users"
|
|
51
|
+
idp["_members"] = list(
|
|
52
|
+
get_paginated(
|
|
53
|
+
api_session,
|
|
54
|
+
members_url,
|
|
55
|
+
params={"idpAlias": idp["alias"], "briefRepresentation": True},
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
result.append(idp)
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def transform(idps: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
63
|
+
for idp in idps:
|
|
64
|
+
idp["_member_ids"] = [member["id"] for member in idp["_members"]]
|
|
65
|
+
idp.pop("_members", None)
|
|
66
|
+
return idps
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@timeit
|
|
70
|
+
def load_identityproviders(
|
|
71
|
+
neo4j_session: neo4j.Session,
|
|
72
|
+
data: list[dict[str, Any]],
|
|
73
|
+
realm: str,
|
|
74
|
+
update_tag: int,
|
|
75
|
+
) -> None:
|
|
76
|
+
logger.info(
|
|
77
|
+
"Loading %d Keycloak IdentityProviders (%s) into Neo4j.", len(data), realm
|
|
78
|
+
)
|
|
79
|
+
load(
|
|
80
|
+
neo4j_session,
|
|
81
|
+
KeycloakIdentityProviderSchema(),
|
|
82
|
+
data,
|
|
83
|
+
LASTUPDATED=update_tag,
|
|
84
|
+
REALM=realm,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@timeit
|
|
89
|
+
def cleanup(
|
|
90
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
91
|
+
) -> None:
|
|
92
|
+
GraphJob.from_node_schema(
|
|
93
|
+
KeycloakIdentityProviderSchema(), common_job_parameters
|
|
94
|
+
).run(neo4j_session)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import neo4j
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from cartography.client.core.tx import load
|
|
9
|
+
from cartography.graph.job import GraphJob
|
|
10
|
+
from cartography.intel.keycloak.util import get_paginated
|
|
11
|
+
from cartography.models.keycloak.organization import KeycloakOrganizationSchema
|
|
12
|
+
from cartography.models.keycloak.organizationdomain import (
|
|
13
|
+
KeycloakOrganizationDomainSchema,
|
|
14
|
+
)
|
|
15
|
+
from cartography.util import timeit
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
19
|
+
_TIMEOUT = (60, 60)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@timeit
|
|
23
|
+
def sync(
|
|
24
|
+
neo4j_session: neo4j.Session,
|
|
25
|
+
api_session: requests.Session,
|
|
26
|
+
base_url: str,
|
|
27
|
+
common_job_parameters: dict[str, Any],
|
|
28
|
+
) -> None:
|
|
29
|
+
organizations = get(
|
|
30
|
+
api_session,
|
|
31
|
+
base_url,
|
|
32
|
+
common_job_parameters["REALM"],
|
|
33
|
+
)
|
|
34
|
+
transformed_orgs, transformed_domains = transform(organizations)
|
|
35
|
+
load_organizations(
|
|
36
|
+
neo4j_session,
|
|
37
|
+
transformed_orgs,
|
|
38
|
+
common_job_parameters["REALM"],
|
|
39
|
+
common_job_parameters["UPDATE_TAG"],
|
|
40
|
+
)
|
|
41
|
+
load_org_domains(
|
|
42
|
+
neo4j_session,
|
|
43
|
+
transformed_domains,
|
|
44
|
+
common_job_parameters["REALM"],
|
|
45
|
+
common_job_parameters["UPDATE_TAG"],
|
|
46
|
+
)
|
|
47
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def transform(
|
|
51
|
+
organizations: list[dict[str, Any]],
|
|
52
|
+
) -> Tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
53
|
+
transformed_orgs = []
|
|
54
|
+
transformed_domains = {}
|
|
55
|
+
for org in organizations:
|
|
56
|
+
# Transform members to a list of IDs
|
|
57
|
+
org["_managed_members"] = []
|
|
58
|
+
org["_unmanaged_members"] = []
|
|
59
|
+
for member in org.get("_members", []):
|
|
60
|
+
if member.get("membershipType") == "UNMANAGED":
|
|
61
|
+
org["_unmanaged_members"].append(member["id"])
|
|
62
|
+
else:
|
|
63
|
+
org["_managed_members"].append(member["id"])
|
|
64
|
+
org.pop("_members", None)
|
|
65
|
+
# Transform identity providers to a list of IDs
|
|
66
|
+
org["_idp_ids"] = [
|
|
67
|
+
idp["internalId"] for idp in org.get("_identity_providers", [])
|
|
68
|
+
]
|
|
69
|
+
org.pop("_identity_providers", None)
|
|
70
|
+
# Extract domains
|
|
71
|
+
domains = org.get("domains", [])
|
|
72
|
+
for domain in domains:
|
|
73
|
+
domain_id = f"{org['id']}-{domain['name']}"
|
|
74
|
+
transformed_domains[domain_id] = {
|
|
75
|
+
"id": domain_id,
|
|
76
|
+
"verified": domain.get("verified", False),
|
|
77
|
+
"name": domain["name"],
|
|
78
|
+
"organization_id": org["id"],
|
|
79
|
+
}
|
|
80
|
+
org.pop("domains", None)
|
|
81
|
+
transformed_orgs.append(org)
|
|
82
|
+
return transformed_orgs, list(transformed_domains.values())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@timeit
|
|
86
|
+
def get(
|
|
87
|
+
api_session: requests.Session,
|
|
88
|
+
base_url: str,
|
|
89
|
+
realm: str,
|
|
90
|
+
) -> list[dict[str, Any]]:
|
|
91
|
+
result: list[dict[str, Any]] = []
|
|
92
|
+
url = f"{base_url}/admin/realms/{realm}/organizations"
|
|
93
|
+
for org in get_paginated(api_session, url):
|
|
94
|
+
# Get members
|
|
95
|
+
members_url = (
|
|
96
|
+
f"{base_url}/admin/realms/{realm}/organizations/{org['id']}/members"
|
|
97
|
+
)
|
|
98
|
+
org["_members"] = list(
|
|
99
|
+
get_paginated(
|
|
100
|
+
api_session,
|
|
101
|
+
members_url,
|
|
102
|
+
params={"briefRepresentation": True},
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
# Get Identity Providers
|
|
106
|
+
idp_url = f"{base_url}/admin/realms/{realm}/organizations/{org['id']}/identity-providers"
|
|
107
|
+
org["_identity_providers"] = list(
|
|
108
|
+
get_paginated(
|
|
109
|
+
api_session,
|
|
110
|
+
idp_url,
|
|
111
|
+
params={"briefRepresentation": True},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
result.append(org)
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@timeit
|
|
119
|
+
def load_organizations(
|
|
120
|
+
neo4j_session: neo4j.Session,
|
|
121
|
+
data: list[dict[str, Any]],
|
|
122
|
+
realm: str,
|
|
123
|
+
update_tag: int,
|
|
124
|
+
) -> None:
|
|
125
|
+
logger.info("Loading %d Keycloak Organizations (%s) into Neo4j.", len(data), realm)
|
|
126
|
+
load(
|
|
127
|
+
neo4j_session,
|
|
128
|
+
KeycloakOrganizationSchema(),
|
|
129
|
+
data,
|
|
130
|
+
LASTUPDATED=update_tag,
|
|
131
|
+
REALM=realm,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@timeit
|
|
136
|
+
def load_org_domains(
|
|
137
|
+
neo4j_session: neo4j.Session,
|
|
138
|
+
data: list[dict[str, Any]],
|
|
139
|
+
realm: str,
|
|
140
|
+
update_tag: int,
|
|
141
|
+
) -> None:
|
|
142
|
+
logger.info(
|
|
143
|
+
"Loading %d Keycloak Organization Domains (%s) into Neo4j.", len(data), realm
|
|
144
|
+
)
|
|
145
|
+
load(
|
|
146
|
+
neo4j_session,
|
|
147
|
+
KeycloakOrganizationDomainSchema(),
|
|
148
|
+
data,
|
|
149
|
+
LASTUPDATED=update_tag,
|
|
150
|
+
REALM=realm,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@timeit
|
|
155
|
+
def cleanup(
|
|
156
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
157
|
+
) -> None:
|
|
158
|
+
GraphJob.from_node_schema(
|
|
159
|
+
KeycloakOrganizationDomainSchema(), common_job_parameters
|
|
160
|
+
).run(neo4j_session)
|
|
161
|
+
GraphJob.from_node_schema(KeycloakOrganizationSchema(), common_job_parameters).run(
|
|
162
|
+
neo4j_session
|
|
163
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from cartography.client.core.tx import load
|
|
8
|
+
from cartography.graph.job import GraphJob
|
|
9
|
+
from cartography.models.keycloak.realm import KeycloakRealmSchema
|
|
10
|
+
from cartography.util import timeit
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
14
|
+
_TIMEOUT = (60, 60)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@timeit
|
|
18
|
+
def sync(
|
|
19
|
+
neo4j_session: neo4j.Session,
|
|
20
|
+
api_session: requests.Session,
|
|
21
|
+
base_url: str,
|
|
22
|
+
common_job_parameters: dict[str, Any],
|
|
23
|
+
) -> list[dict]:
|
|
24
|
+
realms = get(api_session, base_url)
|
|
25
|
+
load_realms(neo4j_session, realms, common_job_parameters["UPDATE_TAG"])
|
|
26
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
27
|
+
return realms
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@timeit
|
|
31
|
+
def get(
|
|
32
|
+
api_session: requests.Session,
|
|
33
|
+
base_url: str,
|
|
34
|
+
) -> list[dict[str, Any]]:
|
|
35
|
+
req = api_session.get(f"{base_url}/admin/realms", timeout=_TIMEOUT)
|
|
36
|
+
req.raise_for_status()
|
|
37
|
+
return req.json()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@timeit
|
|
41
|
+
def load_realms(
|
|
42
|
+
neo4j_session: neo4j.Session,
|
|
43
|
+
data: list[dict[str, Any]],
|
|
44
|
+
update_tag: int,
|
|
45
|
+
) -> None:
|
|
46
|
+
logger.info("Loading %d Keycloak Realms into Neo4j.", len(data))
|
|
47
|
+
load(
|
|
48
|
+
neo4j_session,
|
|
49
|
+
KeycloakRealmSchema(),
|
|
50
|
+
data,
|
|
51
|
+
LASTUPDATED=update_tag,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@timeit
|
|
56
|
+
def cleanup(
|
|
57
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
58
|
+
) -> None:
|
|
59
|
+
GraphJob.from_node_schema(KeycloakRealmSchema(), common_job_parameters).run(
|
|
60
|
+
neo4j_session
|
|
61
|
+
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
import neo4j
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from cartography.client.core.tx import load
|
|
9
|
+
from cartography.graph.job import GraphJob
|
|
10
|
+
from cartography.intel.keycloak.util import get_paginated
|
|
11
|
+
from cartography.models.keycloak.role import KeycloakRoleSchema
|
|
12
|
+
from cartography.util import timeit
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
16
|
+
_TIMEOUT = (60, 60)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@timeit
|
|
20
|
+
def sync(
|
|
21
|
+
neo4j_session: neo4j.Session,
|
|
22
|
+
api_session: requests.Session,
|
|
23
|
+
base_url: str,
|
|
24
|
+
common_job_parameters: dict[str, Any],
|
|
25
|
+
client_ids: list[str],
|
|
26
|
+
scope_ids: list[str],
|
|
27
|
+
) -> None:
|
|
28
|
+
roles = get(api_session, base_url, common_job_parameters["REALM"], client_ids)
|
|
29
|
+
scope_role_mapping = get_mapping(
|
|
30
|
+
api_session,
|
|
31
|
+
base_url,
|
|
32
|
+
common_job_parameters["REALM"],
|
|
33
|
+
scope_ids,
|
|
34
|
+
)
|
|
35
|
+
transformed_roles = transform(roles, scope_role_mapping)
|
|
36
|
+
load_roles(
|
|
37
|
+
neo4j_session,
|
|
38
|
+
transformed_roles,
|
|
39
|
+
common_job_parameters["REALM"],
|
|
40
|
+
common_job_parameters["UPDATE_TAG"],
|
|
41
|
+
)
|
|
42
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@timeit
|
|
46
|
+
def get(
|
|
47
|
+
api_session: requests.Session, base_url: str, realm: str, client_ids: list[str]
|
|
48
|
+
) -> list[dict[str, Any]]:
|
|
49
|
+
roles_by_id: dict[str, dict[str, Any]] = {}
|
|
50
|
+
|
|
51
|
+
# Get roles at the REALM level
|
|
52
|
+
url = f"{base_url}/admin/realms/{realm}/roles"
|
|
53
|
+
for role in get_paginated(api_session, url, params={"briefRepresentation": False}):
|
|
54
|
+
if role.get("composite", False):
|
|
55
|
+
# If the role is composite, we need to get its composites
|
|
56
|
+
composite_roles = get_paginated(
|
|
57
|
+
api_session,
|
|
58
|
+
f"{base_url}/admin/realms/{realm}/roles-by-id/{role['id']}/composites",
|
|
59
|
+
)
|
|
60
|
+
role["_composite_roles"] = [
|
|
61
|
+
composite_role["id"] for composite_role in composite_roles
|
|
62
|
+
]
|
|
63
|
+
# Get the direct members
|
|
64
|
+
direct_members = get_paginated(
|
|
65
|
+
api_session,
|
|
66
|
+
f"{base_url}/admin/realms/{realm}/roles/{quote(role['name'])}/users",
|
|
67
|
+
)
|
|
68
|
+
role["_direct_members"] = [member["id"] for member in direct_members]
|
|
69
|
+
roles_by_id[role["id"]] = role
|
|
70
|
+
|
|
71
|
+
# Get roles for each client
|
|
72
|
+
for client_id in client_ids:
|
|
73
|
+
url = f"{base_url}/admin/realms/{realm}/clients/{client_id}/roles"
|
|
74
|
+
for role in get_paginated(
|
|
75
|
+
api_session, url, params={"briefRepresentation": False}
|
|
76
|
+
):
|
|
77
|
+
# If the role is composite, we need to get its composites
|
|
78
|
+
if role.get("composite", False):
|
|
79
|
+
composite_roles = get_paginated(
|
|
80
|
+
api_session,
|
|
81
|
+
f"{base_url}/admin/realms/{realm}/roles-by-id/{role['id']}/composites",
|
|
82
|
+
)
|
|
83
|
+
role["_composite_roles"] = [
|
|
84
|
+
composite_role["id"] for composite_role in composite_roles
|
|
85
|
+
]
|
|
86
|
+
# Get the direct members
|
|
87
|
+
direct_members = get_paginated(
|
|
88
|
+
api_session,
|
|
89
|
+
f"{base_url}/admin/realms/{realm}/clients/{client_id}/roles/{quote(role['name'])}/users",
|
|
90
|
+
)
|
|
91
|
+
role["_direct_members"] = [member["id"] for member in direct_members]
|
|
92
|
+
roles_by_id[role["id"]] = role
|
|
93
|
+
return list(roles_by_id.values())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@timeit
|
|
97
|
+
def get_mapping(
|
|
98
|
+
api_session: requests.Session,
|
|
99
|
+
base_url: str,
|
|
100
|
+
realm: str,
|
|
101
|
+
scope_ids: list[str],
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
result: dict[str, Any] = {}
|
|
104
|
+
for scope_id in scope_ids:
|
|
105
|
+
mappings_url = (
|
|
106
|
+
f"{base_url}/admin/realms/{realm}/client-scopes/{scope_id}/scope-mappings"
|
|
107
|
+
)
|
|
108
|
+
req = api_session.get(
|
|
109
|
+
mappings_url,
|
|
110
|
+
timeout=_TIMEOUT,
|
|
111
|
+
)
|
|
112
|
+
req.raise_for_status()
|
|
113
|
+
result[scope_id] = req.json()
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def transform(
|
|
118
|
+
roles: list[dict[str, Any]], scope_role_mapping: dict[str, Any]
|
|
119
|
+
) -> list[dict[str, Any]]:
|
|
120
|
+
transformed_roles = []
|
|
121
|
+
|
|
122
|
+
# Transform the mapping of scopes to roles
|
|
123
|
+
scopes_by_roles: dict[str, list[str]] = {}
|
|
124
|
+
for scope_id, mapping in scope_role_mapping.items():
|
|
125
|
+
for client_details in mapping.get("clientMappings", {}).values():
|
|
126
|
+
for client_mapping in client_details.get("mappings", []):
|
|
127
|
+
scopes_by_roles.setdefault(client_mapping["id"], []).append(scope_id)
|
|
128
|
+
for realm_mapping in mapping.get("realmMappings", []):
|
|
129
|
+
scopes_by_roles.setdefault(realm_mapping["id"], []).append(scope_id)
|
|
130
|
+
|
|
131
|
+
for role in roles:
|
|
132
|
+
role["_scope_ids"] = scopes_by_roles.get(role["id"], None)
|
|
133
|
+
transformed_roles.append(role)
|
|
134
|
+
|
|
135
|
+
return _order_dicts_dfs(
|
|
136
|
+
transformed_roles, children_key="_composite_roles", ignore_unknown_children=True
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@timeit
|
|
141
|
+
def load_roles(
|
|
142
|
+
neo4j_session: neo4j.Session,
|
|
143
|
+
data: list[dict[str, Any]],
|
|
144
|
+
realm: str,
|
|
145
|
+
update_tag: int,
|
|
146
|
+
) -> None:
|
|
147
|
+
logger.info("Loading %d Keycloak Roles (%s) into Neo4j.", len(data), realm)
|
|
148
|
+
load(
|
|
149
|
+
neo4j_session,
|
|
150
|
+
KeycloakRoleSchema(),
|
|
151
|
+
data,
|
|
152
|
+
LASTUPDATED=update_tag,
|
|
153
|
+
REALM=realm,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@timeit
|
|
158
|
+
def cleanup(
|
|
159
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
160
|
+
) -> None:
|
|
161
|
+
GraphJob.from_node_schema(KeycloakRoleSchema(), common_job_parameters).run(
|
|
162
|
+
neo4j_session
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _order_dicts_dfs(
|
|
167
|
+
nodes: list[dict[str, Any]],
|
|
168
|
+
children_key: str = "children",
|
|
169
|
+
ignore_unknown_children: bool = False,
|
|
170
|
+
) -> list[dict[str, Any]]:
|
|
171
|
+
by_id = {n["id"]: n for n in nodes}
|
|
172
|
+
|
|
173
|
+
WHITE, GRAY, BLACK = 0, 1, 2 # unvisited, in stack, done
|
|
174
|
+
state: dict[str, int] = {}
|
|
175
|
+
ordered: list[dict[str, Any]] = []
|
|
176
|
+
|
|
177
|
+
def visit(node_id: str, path: list[str]) -> None:
|
|
178
|
+
s = state.get(node_id, WHITE)
|
|
179
|
+
if s == GRAY:
|
|
180
|
+
cycle = " -> ".join(path + [node_id])
|
|
181
|
+
raise ValueError(f"Cycle detected: {cycle}")
|
|
182
|
+
if s == BLACK:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
state[node_id] = GRAY
|
|
186
|
+
node = by_id[node_id]
|
|
187
|
+
|
|
188
|
+
for child_id in node.get(children_key, []):
|
|
189
|
+
if child_id not in by_id:
|
|
190
|
+
if ignore_unknown_children:
|
|
191
|
+
continue
|
|
192
|
+
raise KeyError(f"Unknown child id: {child_id}")
|
|
193
|
+
visit(child_id, path + [node_id])
|
|
194
|
+
|
|
195
|
+
state[node_id] = BLACK
|
|
196
|
+
ordered.append(node) # post-order: children before parent
|
|
197
|
+
|
|
198
|
+
for nid in by_id:
|
|
199
|
+
if state.get(nid, WHITE) == WHITE:
|
|
200
|
+
visit(nid, [])
|
|
201
|
+
|
|
202
|
+
return ordered
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from cartography.client.core.tx import load
|
|
8
|
+
from cartography.graph.job import GraphJob
|
|
9
|
+
from cartography.intel.keycloak.util import get_paginated
|
|
10
|
+
from cartography.models.keycloak.scope import KeycloakScopeSchema
|
|
11
|
+
from cartography.util import timeit
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
15
|
+
_TIMEOUT = (60, 60)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@timeit
|
|
19
|
+
def sync(
|
|
20
|
+
neo4j_session: neo4j.Session,
|
|
21
|
+
api_session: requests.Session,
|
|
22
|
+
base_url: str,
|
|
23
|
+
common_job_parameters: dict[str, Any],
|
|
24
|
+
) -> list[dict[str, Any]]:
|
|
25
|
+
scopes = get(
|
|
26
|
+
api_session,
|
|
27
|
+
base_url,
|
|
28
|
+
common_job_parameters["REALM"],
|
|
29
|
+
)
|
|
30
|
+
load_scopes(
|
|
31
|
+
neo4j_session,
|
|
32
|
+
scopes,
|
|
33
|
+
common_job_parameters["REALM"],
|
|
34
|
+
common_job_parameters["UPDATE_TAG"],
|
|
35
|
+
)
|
|
36
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
37
|
+
return scopes
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@timeit
|
|
41
|
+
def get(
|
|
42
|
+
api_session: requests.Session,
|
|
43
|
+
base_url: str,
|
|
44
|
+
realm: str,
|
|
45
|
+
) -> list[dict[str, Any]]:
|
|
46
|
+
url = f"{base_url}/admin/realms/{realm}/client-scopes"
|
|
47
|
+
return list(get_paginated(api_session, url, params={"briefRepresentation": False}))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@timeit
|
|
51
|
+
def load_scopes(
|
|
52
|
+
neo4j_session: neo4j.Session,
|
|
53
|
+
data: list[dict[str, Any]],
|
|
54
|
+
realm: str,
|
|
55
|
+
update_tag: int,
|
|
56
|
+
) -> None:
|
|
57
|
+
logger.info("Loading %d Keycloak Scopes (%s) into Neo4j.", len(data), realm)
|
|
58
|
+
load(
|
|
59
|
+
neo4j_session,
|
|
60
|
+
KeycloakScopeSchema(),
|
|
61
|
+
data,
|
|
62
|
+
LASTUPDATED=update_tag,
|
|
63
|
+
REALM=realm,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@timeit
|
|
68
|
+
def cleanup(
|
|
69
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
70
|
+
) -> None:
|
|
71
|
+
GraphJob.from_node_schema(KeycloakScopeSchema(), common_job_parameters).run(
|
|
72
|
+
neo4j_session
|
|
73
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from cartography.client.core.tx import load
|
|
8
|
+
from cartography.graph.job import GraphJob
|
|
9
|
+
from cartography.intel.keycloak.util import get_paginated
|
|
10
|
+
from cartography.models.keycloak.user import KeycloakUserSchema
|
|
11
|
+
from cartography.util import timeit
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@timeit
|
|
17
|
+
def sync(
|
|
18
|
+
neo4j_session: neo4j.Session,
|
|
19
|
+
api_session: requests.Session,
|
|
20
|
+
base_url: str,
|
|
21
|
+
common_job_parameters: dict[str, Any],
|
|
22
|
+
) -> None:
|
|
23
|
+
users = get(
|
|
24
|
+
api_session,
|
|
25
|
+
base_url,
|
|
26
|
+
common_job_parameters["REALM"],
|
|
27
|
+
)
|
|
28
|
+
load_users(
|
|
29
|
+
neo4j_session,
|
|
30
|
+
users,
|
|
31
|
+
common_job_parameters["REALM"],
|
|
32
|
+
common_job_parameters["UPDATE_TAG"],
|
|
33
|
+
)
|
|
34
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@timeit
|
|
38
|
+
def get(
|
|
39
|
+
api_session: requests.Session,
|
|
40
|
+
base_url: str,
|
|
41
|
+
realm: str,
|
|
42
|
+
) -> list[dict[str, Any]]:
|
|
43
|
+
url = f"{base_url}/admin/realms/{realm}/users"
|
|
44
|
+
return list(get_paginated(api_session, url))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@timeit
|
|
48
|
+
def load_users(
|
|
49
|
+
neo4j_session: neo4j.Session,
|
|
50
|
+
data: list[dict[str, Any]],
|
|
51
|
+
realm: str,
|
|
52
|
+
update_tag: int,
|
|
53
|
+
) -> None:
|
|
54
|
+
logger.info("Loading %d Keycloak Users (%s) into Neo4j.", len(data), realm)
|
|
55
|
+
load(
|
|
56
|
+
neo4j_session,
|
|
57
|
+
KeycloakUserSchema(),
|
|
58
|
+
data,
|
|
59
|
+
LASTUPDATED=update_tag,
|
|
60
|
+
REALM=realm,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@timeit
|
|
65
|
+
def cleanup(
|
|
66
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
67
|
+
) -> None:
|
|
68
|
+
GraphJob.from_node_schema(KeycloakUserSchema(), common_job_parameters).run(
|
|
69
|
+
neo4j_session
|
|
70
|
+
)
|