cartography 0.110.0rc1__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 -8
- cartography/config.py +16 -9
- cartography/data/indexes.cypher +0 -2
- cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json +2 -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/cognito.py +201 -0
- cartography/intel/aws/ec2/vpc.py +140 -124
- cartography/intel/aws/ecs.py +7 -1
- cartography/intel/aws/eventbridge.py +73 -0
- cartography/intel/aws/glue.py +64 -0
- cartography/intel/aws/kms.py +13 -1
- cartography/intel/aws/rds.py +105 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/aws/route53.py +3 -1
- cartography/intel/aws/s3.py +104 -0
- cartography/intel/entra/__init__.py +41 -43
- cartography/intel/entra/applications.py +2 -1
- cartography/intel/entra/ou.py +1 -1
- cartography/intel/github/__init__.py +21 -25
- cartography/intel/github/repos.py +32 -48
- 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/intel/kubernetes/__init__.py +4 -0
- cartography/intel/kubernetes/rbac.py +464 -0
- cartography/intel/kubernetes/util.py +17 -0
- cartography/models/aws/apigateway/apigatewaydeployment.py +74 -0
- cartography/models/aws/cognito/__init__.py +0 -0
- cartography/models/aws/cognito/identity_pool.py +70 -0
- cartography/models/aws/cognito/user_pool.py +47 -0
- cartography/models/aws/ec2/security_groups.py +1 -1
- cartography/models/aws/ec2/vpc.py +46 -0
- cartography/models/aws/ec2/vpc_cidr.py +102 -0
- cartography/models/aws/ecs/services.py +17 -0
- cartography/models/aws/ecs/tasks.py +1 -0
- cartography/models/aws/eventbridge/target.py +71 -0
- cartography/models/aws/glue/job.py +69 -0
- cartography/models/aws/rds/event_subscription.py +146 -0
- cartography/models/aws/route53/dnsrecord.py +21 -0
- cartography/models/github/dependencies.py +1 -2
- 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/kubernetes/clusterrolebindings.py +98 -0
- cartography/models/kubernetes/clusterroles.py +52 -0
- cartography/models/kubernetes/rolebindings.py +119 -0
- cartography/models/kubernetes/roles.py +76 -0
- cartography/models/kubernetes/serviceaccounts.py +77 -0
- cartography/models/tailscale/device.py +1 -0
- cartography/sync.py +2 -0
- cartography/util.py +8 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/METADATA +4 -3
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/RECORD +85 -46
- cartography/data/jobs/cleanup/aws_import_vpc_cleanup.json +0 -23
- cartography/intel/entra/resources.py +0 -20
- /cartography/data/jobs/{analysis → scoped_analysis}/aws_s3acl_analysis.json +0 -0
- /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.0rc1.dist-info → cartography-0.111.0.dist-info}/WHEEL +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
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.authenticationflow import (
|
|
11
|
+
KeycloakAuthenticationFlowSchema,
|
|
12
|
+
)
|
|
13
|
+
from cartography.util import timeit
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
17
|
+
_TIMEOUT = (60, 60)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@timeit
|
|
21
|
+
def sync(
|
|
22
|
+
neo4j_session: neo4j.Session,
|
|
23
|
+
api_session: requests.Session,
|
|
24
|
+
base_url: str,
|
|
25
|
+
common_job_parameters: dict[str, Any],
|
|
26
|
+
) -> list[dict[str, Any]]:
|
|
27
|
+
authenticationflows = get(
|
|
28
|
+
api_session,
|
|
29
|
+
base_url,
|
|
30
|
+
common_job_parameters["REALM"],
|
|
31
|
+
)
|
|
32
|
+
load_authenticationflows(
|
|
33
|
+
neo4j_session,
|
|
34
|
+
authenticationflows,
|
|
35
|
+
common_job_parameters["REALM"],
|
|
36
|
+
common_job_parameters["UPDATE_TAG"],
|
|
37
|
+
)
|
|
38
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
39
|
+
return authenticationflows
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@timeit
|
|
43
|
+
def get(
|
|
44
|
+
api_session: requests.Session,
|
|
45
|
+
base_url: str,
|
|
46
|
+
realm: str,
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
48
|
+
url = f"{base_url}/admin/realms/{realm}/authentication/flows"
|
|
49
|
+
return list(get_paginated(api_session, url, params={"briefRepresentation": False}))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@timeit
|
|
53
|
+
def load_authenticationflows(
|
|
54
|
+
neo4j_session: neo4j.Session,
|
|
55
|
+
data: list[dict[str, Any]],
|
|
56
|
+
realm: str,
|
|
57
|
+
update_tag: int,
|
|
58
|
+
) -> None:
|
|
59
|
+
logger.info(
|
|
60
|
+
"Loading %d Keycloak AuthenticationFlows (%s) into Neo4j.", len(data), realm
|
|
61
|
+
)
|
|
62
|
+
load(
|
|
63
|
+
neo4j_session,
|
|
64
|
+
KeycloakAuthenticationFlowSchema(),
|
|
65
|
+
data,
|
|
66
|
+
LASTUPDATED=update_tag,
|
|
67
|
+
REALM=realm,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@timeit
|
|
72
|
+
def cleanup(
|
|
73
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
74
|
+
) -> None:
|
|
75
|
+
GraphJob.from_node_schema(
|
|
76
|
+
KeycloakAuthenticationFlowSchema(), common_job_parameters
|
|
77
|
+
).run(neo4j_session)
|
|
@@ -0,0 +1,187 @@
|
|
|
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.client.core.tx import load_matchlinks
|
|
9
|
+
from cartography.graph.job import GraphJob
|
|
10
|
+
from cartography.intel.keycloak.util import get_paginated
|
|
11
|
+
from cartography.models.keycloak.client import KeycloakClientSchema
|
|
12
|
+
from cartography.models.keycloak.client import KeycloakClientToFlowMatchLink
|
|
13
|
+
from cartography.models.keycloak.user import KeycloakUserSchema
|
|
14
|
+
from cartography.util import timeit
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
18
|
+
_TIMEOUT = (60, 60)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@timeit
|
|
22
|
+
def sync(
|
|
23
|
+
neo4j_session: neo4j.Session,
|
|
24
|
+
api_session: requests.Session,
|
|
25
|
+
base_url: str,
|
|
26
|
+
common_job_parameters: dict[str, Any],
|
|
27
|
+
realm_default_flows: dict[str, Any],
|
|
28
|
+
) -> list[dict]:
|
|
29
|
+
clients = get(
|
|
30
|
+
api_session,
|
|
31
|
+
base_url,
|
|
32
|
+
common_job_parameters["REALM"],
|
|
33
|
+
)
|
|
34
|
+
transformed_clients, service_accounts, flows_binding = transform(
|
|
35
|
+
clients, realm_default_flows
|
|
36
|
+
)
|
|
37
|
+
load_service_accounts(
|
|
38
|
+
neo4j_session,
|
|
39
|
+
service_accounts,
|
|
40
|
+
common_job_parameters["REALM"],
|
|
41
|
+
common_job_parameters["UPDATE_TAG"],
|
|
42
|
+
)
|
|
43
|
+
load_clients(
|
|
44
|
+
neo4j_session,
|
|
45
|
+
transformed_clients,
|
|
46
|
+
common_job_parameters["REALM"],
|
|
47
|
+
common_job_parameters["UPDATE_TAG"],
|
|
48
|
+
)
|
|
49
|
+
load_flow_bindings(
|
|
50
|
+
neo4j_session,
|
|
51
|
+
flows_binding,
|
|
52
|
+
common_job_parameters["REALM_ID"],
|
|
53
|
+
common_job_parameters["UPDATE_TAG"],
|
|
54
|
+
)
|
|
55
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
56
|
+
return clients
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@timeit
|
|
60
|
+
def get(
|
|
61
|
+
api_session: requests.Session,
|
|
62
|
+
base_url: str,
|
|
63
|
+
realm: str,
|
|
64
|
+
) -> list[dict[str, Any]]:
|
|
65
|
+
result: list[dict[str, Any]] = []
|
|
66
|
+
url = f"{base_url}/admin/realms/{realm}/clients"
|
|
67
|
+
for client in get_paginated(
|
|
68
|
+
api_session, url, params={"briefRepresentation": False}
|
|
69
|
+
):
|
|
70
|
+
# Check if the client has a service account user
|
|
71
|
+
if "service_account" in client.get("defaultClientScopes", []):
|
|
72
|
+
# Get service account user for each client
|
|
73
|
+
service_account_url = f"{base_url}/admin/realms/{realm}/clients/{client['id']}/service-account-user"
|
|
74
|
+
sa_req = api_session.get(
|
|
75
|
+
service_account_url,
|
|
76
|
+
timeout=_TIMEOUT,
|
|
77
|
+
params={"briefRepresentation": False},
|
|
78
|
+
)
|
|
79
|
+
sa_req.raise_for_status()
|
|
80
|
+
client["service_account_user"] = sa_req.json()
|
|
81
|
+
result.append(client)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def transform(
|
|
86
|
+
clients: list[dict[str, Any]], default_flows: dict[str, Any]
|
|
87
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
88
|
+
transformed_clients = []
|
|
89
|
+
service_accounts = []
|
|
90
|
+
flow_bindings = []
|
|
91
|
+
for client in clients:
|
|
92
|
+
sa = client.get("service_account_user")
|
|
93
|
+
if sa:
|
|
94
|
+
service_accounts.append(sa)
|
|
95
|
+
client["_service_account_user_id"] = sa["id"]
|
|
96
|
+
client.pop("service_account_user", None)
|
|
97
|
+
|
|
98
|
+
for flow_name, default_flow_id in default_flows.items():
|
|
99
|
+
flow_binding = {
|
|
100
|
+
"client_id": client["id"],
|
|
101
|
+
"flow_name": flow_name,
|
|
102
|
+
"flow_id": default_flow_id,
|
|
103
|
+
"default_flow": True,
|
|
104
|
+
}
|
|
105
|
+
if client.get("authenticationFlowBindingOverrides", {}).get(flow_name):
|
|
106
|
+
flow_binding["flow_id"] = client["authenticationFlowBindingOverrides"][
|
|
107
|
+
flow_name
|
|
108
|
+
]
|
|
109
|
+
flow_binding["default_flow"] = False
|
|
110
|
+
flow_bindings.append(flow_binding)
|
|
111
|
+
|
|
112
|
+
transformed_clients.append(client)
|
|
113
|
+
return transformed_clients, service_accounts, flow_bindings
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@timeit
|
|
117
|
+
def load_clients(
|
|
118
|
+
neo4j_session: neo4j.Session,
|
|
119
|
+
data: list[dict[str, Any]],
|
|
120
|
+
realm: str,
|
|
121
|
+
update_tag: int,
|
|
122
|
+
) -> None:
|
|
123
|
+
logger.info("Loading %d Keycloak Clients (%s) into Neo4j.", len(data), realm)
|
|
124
|
+
load(
|
|
125
|
+
neo4j_session,
|
|
126
|
+
KeycloakClientSchema(),
|
|
127
|
+
data,
|
|
128
|
+
LASTUPDATED=update_tag,
|
|
129
|
+
REALM=realm,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@timeit
|
|
134
|
+
def load_service_accounts(
|
|
135
|
+
neo4j_session: neo4j.Session,
|
|
136
|
+
data: list[dict[str, Any]],
|
|
137
|
+
realm: str,
|
|
138
|
+
update_tag: int,
|
|
139
|
+
) -> None:
|
|
140
|
+
logger.info(
|
|
141
|
+
"Loading %d Keycloak Service Accounts (%s) into Neo4j.", len(data), realm
|
|
142
|
+
)
|
|
143
|
+
load(
|
|
144
|
+
neo4j_session,
|
|
145
|
+
KeycloakUserSchema(),
|
|
146
|
+
data,
|
|
147
|
+
LASTUPDATED=update_tag,
|
|
148
|
+
REALM=realm,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@timeit
|
|
153
|
+
def load_flow_bindings(
|
|
154
|
+
neo4j_session: neo4j.Session,
|
|
155
|
+
biddings: list[dict[str, Any]],
|
|
156
|
+
realm_id: str,
|
|
157
|
+
update_tag: int,
|
|
158
|
+
) -> None:
|
|
159
|
+
load_matchlinks(
|
|
160
|
+
neo4j_session,
|
|
161
|
+
KeycloakClientToFlowMatchLink(),
|
|
162
|
+
biddings,
|
|
163
|
+
LASTUPDATED=update_tag,
|
|
164
|
+
_sub_resource_label="KeycloakRealm",
|
|
165
|
+
_sub_resource_id=realm_id,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@timeit
|
|
170
|
+
def cleanup(
|
|
171
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
172
|
+
) -> None:
|
|
173
|
+
GraphJob.from_node_schema(KeycloakClientSchema(), common_job_parameters).run(
|
|
174
|
+
neo4j_session
|
|
175
|
+
)
|
|
176
|
+
# It's OK to cleanup users here as it will not clean the regular users (which have the same UPDATE_TAG)
|
|
177
|
+
GraphJob.from_node_schema(KeycloakUserSchema(), common_job_parameters).run(
|
|
178
|
+
neo4j_session
|
|
179
|
+
)
|
|
180
|
+
GraphJob.from_matchlink(
|
|
181
|
+
KeycloakClientToFlowMatchLink(),
|
|
182
|
+
sub_resource_label="KeycloakRealm",
|
|
183
|
+
sub_resource_id=common_job_parameters["REALM_ID"],
|
|
184
|
+
update_tag=common_job_parameters["UPDATE_TAG"],
|
|
185
|
+
).run(
|
|
186
|
+
neo4j_session,
|
|
187
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
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.group import KeycloakGroupSchema
|
|
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
|
+
groups = get(
|
|
24
|
+
api_session,
|
|
25
|
+
base_url,
|
|
26
|
+
common_job_parameters["REALM"],
|
|
27
|
+
)
|
|
28
|
+
transformed_groups = transform(groups)
|
|
29
|
+
load_groups(
|
|
30
|
+
neo4j_session,
|
|
31
|
+
transformed_groups,
|
|
32
|
+
common_job_parameters["REALM"],
|
|
33
|
+
common_job_parameters["UPDATE_TAG"],
|
|
34
|
+
)
|
|
35
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@timeit
|
|
39
|
+
def _get_subgroups(
|
|
40
|
+
api_session: requests.Session,
|
|
41
|
+
base_url: str,
|
|
42
|
+
realm: str,
|
|
43
|
+
group_id: str,
|
|
44
|
+
) -> list[dict[str, Any]]:
|
|
45
|
+
result: list[dict[str, Any]] = []
|
|
46
|
+
url = f"{base_url}/admin/realms/{realm}/groups/{group_id}/children"
|
|
47
|
+
for group in get_paginated(
|
|
48
|
+
api_session,
|
|
49
|
+
url,
|
|
50
|
+
):
|
|
51
|
+
group["_members"] = _get_members(api_session, base_url, realm, group["id"])
|
|
52
|
+
result.append(group)
|
|
53
|
+
if group.get("subGroupCount", 0) > 0:
|
|
54
|
+
result.extend(_get_subgroups(api_session, base_url, realm, group["id"]))
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@timeit
|
|
59
|
+
def _get_members(
|
|
60
|
+
api_session: requests.Session,
|
|
61
|
+
base_url: str,
|
|
62
|
+
realm: str,
|
|
63
|
+
group_id: str,
|
|
64
|
+
) -> list[dict[str, Any]]:
|
|
65
|
+
url = f"{base_url}/admin/realms/{realm}/groups/{group_id}/members"
|
|
66
|
+
return list(get_paginated(api_session, url))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@timeit
|
|
70
|
+
def get(
|
|
71
|
+
api_session: requests.Session,
|
|
72
|
+
base_url: str,
|
|
73
|
+
realm: str,
|
|
74
|
+
) -> list[dict[str, Any]]:
|
|
75
|
+
result: list[dict[str, Any]] = []
|
|
76
|
+
|
|
77
|
+
url = f"{base_url}/admin/realms/{realm}/groups"
|
|
78
|
+
for group in get_paginated(api_session, url, params={"briefRepresentation": False}):
|
|
79
|
+
group["_members"] = _get_members(api_session, base_url, realm, group["id"])
|
|
80
|
+
result.append(group)
|
|
81
|
+
if group.get("subGroupCount", 0) > 0:
|
|
82
|
+
result.extend(_get_subgroups(api_session, base_url, realm, group["id"]))
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def transform(groups: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
87
|
+
for group in groups:
|
|
88
|
+
# Transform members to a list of IDs for easier relationship handling
|
|
89
|
+
group["_member_ids"] = [m["id"] for m in group["_members"]]
|
|
90
|
+
group.pop("_members")
|
|
91
|
+
# Transform roles to a list of role names for easier relationship handling
|
|
92
|
+
group["_roles"] = []
|
|
93
|
+
for role_name in group.get("realmRoles", []):
|
|
94
|
+
group["_roles"].append(role_name)
|
|
95
|
+
group.pop("realmRoles", None)
|
|
96
|
+
for roles in group.get("clientRoles", {}).values():
|
|
97
|
+
for role_name in roles:
|
|
98
|
+
group["_roles"].append(role_name)
|
|
99
|
+
group.pop("clientRoles", None)
|
|
100
|
+
return groups
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@timeit
|
|
104
|
+
def load_groups(
|
|
105
|
+
neo4j_session: neo4j.Session,
|
|
106
|
+
data: list[dict[str, Any]],
|
|
107
|
+
realm: str,
|
|
108
|
+
update_tag: int,
|
|
109
|
+
) -> None:
|
|
110
|
+
logger.info("Loading %d Keycloak Groups (%s) into Neo4j.", len(data), realm)
|
|
111
|
+
load(
|
|
112
|
+
neo4j_session,
|
|
113
|
+
KeycloakGroupSchema(),
|
|
114
|
+
data,
|
|
115
|
+
LASTUPDATED=update_tag,
|
|
116
|
+
REALM=realm,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@timeit
|
|
121
|
+
def cleanup(
|
|
122
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
123
|
+
) -> None:
|
|
124
|
+
GraphJob.from_node_schema(KeycloakGroupSchema(), common_job_parameters).run(
|
|
125
|
+
neo4j_session
|
|
126
|
+
)
|
|
@@ -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
|
+
)
|