cartography 0.111.0rc1__py3-none-any.whl → 0.112.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 +2 -2
- cartography/cli.py +57 -0
- cartography/config.py +24 -0
- cartography/data/indexes.cypher +0 -2
- cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
- cartography/intel/aws/apigateway.py +128 -17
- cartography/intel/aws/ec2/instances.py +3 -1
- cartography/intel/aws/ec2/network_interfaces.py +1 -1
- cartography/intel/aws/ec2/vpc_peerings.py +262 -125
- cartography/intel/azure/__init__.py +35 -32
- cartography/intel/azure/subscription.py +2 -2
- cartography/intel/azure/tenant.py +39 -30
- cartography/intel/azure/util/credentials.py +49 -174
- cartography/intel/entra/__init__.py +47 -1
- cartography/intel/entra/applications.py +220 -170
- cartography/intel/entra/groups.py +41 -22
- cartography/intel/entra/ou.py +28 -20
- cartography/intel/entra/users.py +24 -18
- cartography/intel/gcp/__init__.py +25 -8
- cartography/intel/gcp/compute.py +47 -12
- cartography/intel/github/repos.py +19 -10
- 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 +26 -0
- cartography/intel/kubernetes/eks.py +402 -0
- cartography/intel/kubernetes/rbac.py +133 -0
- cartography/models/aws/apigateway/apigatewayintegration.py +79 -0
- cartography/models/aws/apigateway/apigatewaymethod.py +74 -0
- cartography/models/aws/ec2/vpc_peering.py +157 -0
- cartography/models/azure/principal.py +44 -0
- cartography/models/azure/tenant.py +20 -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/kubernetes/clusterrolebindings.py +40 -0
- cartography/models/kubernetes/groups.py +107 -0
- cartography/models/kubernetes/oidc.py +51 -0
- cartography/models/kubernetes/rolebindings.py +40 -0
- cartography/models/kubernetes/users.py +105 -0
- cartography/sync.py +2 -0
- cartography/util.py +10 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.112.0.dist-info}/METADATA +9 -5
- {cartography-0.111.0rc1.dist-info → cartography-0.112.0.dist-info}/RECORD +67 -34
- cartography/data/jobs/cleanup/aws_import_vpc_peering_cleanup.json +0 -45
- {cartography-0.111.0rc1.dist-info → cartography-0.112.0.dist-info}/WHEEL +0 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.112.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.112.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.112.0.dist-info}/top_level.txt +0 -0
|
@@ -1,58 +1,67 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import Dict
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
import neo4j
|
|
5
6
|
|
|
6
|
-
from cartography.
|
|
7
|
-
from cartography.
|
|
7
|
+
from cartography.client.core.tx import load
|
|
8
|
+
from cartography.graph.job import GraphJob
|
|
9
|
+
from cartography.models.azure.principal import AzurePrincipalSchema
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
# Import the new, separated schemas
|
|
12
|
+
from cartography.models.azure.tenant import AzureTenantSchema
|
|
13
|
+
from cartography.util import timeit
|
|
10
14
|
|
|
11
15
|
logger = logging.getLogger(__name__)
|
|
12
16
|
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
return credentials.get_tenant_id()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
@timeit
|
|
18
19
|
def load_azure_tenant(
|
|
19
20
|
neo4j_session: neo4j.Session,
|
|
20
21
|
tenant_id: str,
|
|
21
|
-
current_user: str,
|
|
22
|
+
current_user: Optional[str],
|
|
22
23
|
update_tag: int,
|
|
23
24
|
) -> None:
|
|
24
|
-
query = """
|
|
25
|
-
MERGE (at:AzureTenant{id: $TENANT_ID})
|
|
26
|
-
ON CREATE SET at.firstseen = timestamp()
|
|
27
|
-
SET at.lastupdated = $update_tag
|
|
28
|
-
WITH at
|
|
29
|
-
MERGE (ap:AzurePrincipal{id: $CURRENT_USER})
|
|
30
|
-
ON CREATE SET ap.email = $CURRENT_USER, ap.firstseen = timestamp()
|
|
31
|
-
SET ap.lastupdated = $update_tag
|
|
32
|
-
WITH at, ap
|
|
33
|
-
MERGE (at)-[r:RESOURCE]->(ap)
|
|
34
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
35
|
-
SET r.lastupdated = $update_tag;
|
|
36
25
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
update_tag=update_tag,
|
|
42
|
-
)
|
|
26
|
+
Ingest the Azure Tenant and, if available, the Azure Principal into Neo4j.
|
|
27
|
+
"""
|
|
28
|
+
tenant_data = {"id": tenant_id}
|
|
29
|
+
load(neo4j_session, AzureTenantSchema(), [tenant_data], lastupdated=update_tag)
|
|
43
30
|
|
|
31
|
+
if current_user:
|
|
32
|
+
principal_data = {"id": current_user}
|
|
33
|
+
load(
|
|
34
|
+
neo4j_session,
|
|
35
|
+
AzurePrincipalSchema(),
|
|
36
|
+
[principal_data],
|
|
37
|
+
lastupdated=update_tag,
|
|
38
|
+
TENANT_ID=tenant_id,
|
|
39
|
+
)
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
|
|
42
|
+
@timeit
|
|
43
|
+
def cleanup_azure_tenant(
|
|
44
|
+
neo4j_session: neo4j.Session, common_job_parameters: Dict
|
|
45
|
+
) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Delete stale Azure Tenants and Principals.
|
|
48
|
+
"""
|
|
49
|
+
GraphJob.from_node_schema(AzureTenantSchema(), common_job_parameters).run(
|
|
50
|
+
neo4j_session
|
|
51
|
+
)
|
|
52
|
+
GraphJob.from_node_schema(AzurePrincipalSchema(), common_job_parameters).run(
|
|
53
|
+
neo4j_session
|
|
54
|
+
)
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
@timeit
|
|
50
58
|
def sync(
|
|
51
59
|
neo4j_session: neo4j.Session,
|
|
52
60
|
tenant_id: str,
|
|
53
|
-
current_user: str,
|
|
61
|
+
current_user: Optional[str],
|
|
54
62
|
update_tag: int,
|
|
55
63
|
common_job_parameters: Dict,
|
|
56
64
|
) -> None:
|
|
65
|
+
logger.info("Syncing Azure tenant '%s'.", tenant_id)
|
|
57
66
|
load_azure_tenant(neo4j_session, tenant_id, current_user, update_tag)
|
|
58
|
-
|
|
67
|
+
cleanup_azure_tenant(neo4j_session, common_job_parameters)
|
|
@@ -1,222 +1,97 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from datetime import timedelta
|
|
4
2
|
from typing import Any
|
|
5
3
|
from typing import Optional
|
|
6
4
|
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
from azure.common.credentials import get_azure_cli_credentials
|
|
10
|
-
from azure.common.credentials import get_cli_profile
|
|
11
|
-
from azure.core.exceptions import HttpResponseError
|
|
5
|
+
import jwt
|
|
6
|
+
from azure.identity import AzureCliCredential
|
|
12
7
|
from azure.identity import ClientSecretCredential
|
|
13
|
-
from
|
|
8
|
+
from azure.mgmt.resource import SubscriptionClient
|
|
14
9
|
|
|
15
10
|
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_tenant_id_from_token(credential: Any) -> str:
|
|
14
|
+
"""
|
|
15
|
+
A helper function to get the tenant ID from the claims in an access token.
|
|
16
|
+
"""
|
|
17
|
+
token = credential.get_token("https://management.azure.com/.default")
|
|
18
|
+
decoded_token = jwt.decode(token.token, options={"verify_signature": False})
|
|
19
|
+
return decoded_token.get("tid", "")
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class Credentials:
|
|
23
|
+
"""
|
|
24
|
+
A simple data container for the credential object and its associated IDs.
|
|
25
|
+
"""
|
|
20
26
|
|
|
21
27
|
def __init__(
|
|
22
28
|
self,
|
|
23
|
-
|
|
24
|
-
aad_graph_credentials: Any,
|
|
29
|
+
credential: Any,
|
|
25
30
|
tenant_id: Optional[str] = None,
|
|
26
31
|
subscription_id: Optional[str] = None,
|
|
27
|
-
context: Optional[adal.AuthenticationContext] = None,
|
|
28
|
-
current_user: Optional[str] = None,
|
|
29
32
|
) -> None:
|
|
30
|
-
self.
|
|
31
|
-
self.aad_graph_credentials = (
|
|
32
|
-
aad_graph_credentials # Azure AD Graph API credentials
|
|
33
|
-
)
|
|
33
|
+
self.credential = credential
|
|
34
34
|
self.tenant_id = tenant_id
|
|
35
35
|
self.subscription_id = subscription_id
|
|
36
|
-
self.context = context
|
|
37
|
-
self.current_user = current_user
|
|
38
|
-
|
|
39
|
-
def get_current_user(self) -> Optional[str]:
|
|
40
|
-
return self.current_user
|
|
41
|
-
|
|
42
|
-
def get_tenant_id(self) -> Any:
|
|
43
|
-
if self.tenant_id:
|
|
44
|
-
return self.tenant_id
|
|
45
|
-
elif "tenant_id" in self.aad_graph_credentials.token:
|
|
46
|
-
return self.aad_graph_credentials.token["tenant_id"]
|
|
47
|
-
else:
|
|
48
|
-
# This is a last resort, e.g. for MSI authentication
|
|
49
|
-
try:
|
|
50
|
-
h = {
|
|
51
|
-
"Authorization": "Bearer {}".format(
|
|
52
|
-
self.arm_credentials.token["access_token"],
|
|
53
|
-
),
|
|
54
|
-
}
|
|
55
|
-
r = requests.get(
|
|
56
|
-
"https://management.azure.com/tenants?api-version=2020-01-01",
|
|
57
|
-
headers=h,
|
|
58
|
-
)
|
|
59
|
-
r2 = r.json()
|
|
60
|
-
return r2.get("value")[0].get("tenantId")
|
|
61
|
-
except requests.ConnectionError as e:
|
|
62
|
-
logger.error(f"Unable to infer tenant ID: {e}")
|
|
63
|
-
return None
|
|
64
|
-
|
|
65
|
-
def get_credentials(self, resource: str) -> Any:
|
|
66
|
-
if resource == "arm":
|
|
67
|
-
self.arm_credentials = self.get_fresh_credentials(self.arm_credentials)
|
|
68
|
-
return self.arm_credentials
|
|
69
|
-
elif resource == "aad_graph":
|
|
70
|
-
self.aad_graph_credentials = self.get_fresh_credentials(
|
|
71
|
-
self.aad_graph_credentials,
|
|
72
|
-
)
|
|
73
|
-
return self.aad_graph_credentials
|
|
74
|
-
else:
|
|
75
|
-
raise Exception("Invalid credentials resource type")
|
|
76
|
-
|
|
77
|
-
def get_fresh_credentials(self, credentials: Any) -> Any:
|
|
78
|
-
"""
|
|
79
|
-
Check if credentials are outdated and if so refresh them.
|
|
80
|
-
"""
|
|
81
|
-
if self.context and hasattr(credentials, "token"):
|
|
82
|
-
expiration_datetime = datetime.fromtimestamp(
|
|
83
|
-
credentials.token["expires_on"],
|
|
84
|
-
)
|
|
85
|
-
current_datetime = datetime.now()
|
|
86
|
-
expiration_delta = expiration_datetime - current_datetime
|
|
87
|
-
if expiration_delta < timedelta(minutes=5):
|
|
88
|
-
return self.refresh_credential(credentials)
|
|
89
|
-
return credentials
|
|
90
|
-
|
|
91
|
-
def refresh_credential(self, credentials: Any) -> Any:
|
|
92
|
-
"""
|
|
93
|
-
Refresh credentials
|
|
94
|
-
"""
|
|
95
|
-
logger.debug("Refreshing credentials")
|
|
96
|
-
authority_uri = AUTHORITY_HOST_URI + "/" + self.get_tenant_id()
|
|
97
|
-
if self.context:
|
|
98
|
-
existing_cache = self.context.cache
|
|
99
|
-
context = adal.AuthenticationContext(authority_uri, cache=existing_cache)
|
|
100
|
-
|
|
101
|
-
else:
|
|
102
|
-
context = adal.AuthenticationContext(authority_uri)
|
|
103
|
-
|
|
104
|
-
new_token = context.acquire_token(
|
|
105
|
-
credentials.token["resource"],
|
|
106
|
-
credentials.token["user_id"],
|
|
107
|
-
credentials.token["_client_id"],
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
new_credentials = AADTokenCredentials(
|
|
111
|
-
new_token,
|
|
112
|
-
credentials.token.get("_client_id"),
|
|
113
|
-
)
|
|
114
|
-
return new_credentials
|
|
115
36
|
|
|
116
37
|
|
|
117
38
|
class Authenticator:
|
|
118
39
|
|
|
119
|
-
def authenticate_cli(self) -> Credentials:
|
|
40
|
+
def authenticate_cli(self) -> Optional[Credentials]:
|
|
120
41
|
"""
|
|
121
|
-
Implements authentication
|
|
42
|
+
Implements authentication using the Azure CLI with the modern library.
|
|
122
43
|
"""
|
|
44
|
+
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
|
45
|
+
logging.getLogger(
|
|
46
|
+
"azure.core.pipeline.policies.http_logging_policy",
|
|
47
|
+
).setLevel(logging.ERROR)
|
|
123
48
|
try:
|
|
49
|
+
credential = AzureCliCredential()
|
|
124
50
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
logging.getLogger("msrestazure.azure_active_directory").setLevel(
|
|
129
|
-
logging.ERROR,
|
|
130
|
-
)
|
|
131
|
-
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
|
132
|
-
logging.getLogger(
|
|
133
|
-
"azure.core.pipeline.policies.http_logging_policy",
|
|
134
|
-
).setLevel(logging.ERROR)
|
|
51
|
+
subscription_client = SubscriptionClient(credential)
|
|
52
|
+
subscription = next(subscription_client.subscriptions.list())
|
|
53
|
+
subscription_id = subscription.subscription_id
|
|
135
54
|
|
|
136
|
-
|
|
137
|
-
with_tenant=True,
|
|
138
|
-
)
|
|
139
|
-
aad_graph_credentials, placeholder_1, placeholder_2 = (
|
|
140
|
-
get_azure_cli_credentials(
|
|
141
|
-
with_tenant=True,
|
|
142
|
-
resource="https://graph.windows.net",
|
|
143
|
-
)
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
profile = get_cli_profile()
|
|
55
|
+
tenant_id = _get_tenant_id_from_token(credential)
|
|
147
56
|
|
|
148
57
|
return Credentials(
|
|
149
|
-
|
|
150
|
-
aad_graph_credentials,
|
|
58
|
+
credential=credential,
|
|
151
59
|
tenant_id=tenant_id,
|
|
152
|
-
current_user=profile.get_current_account_user(),
|
|
153
60
|
subscription_id=subscription_id,
|
|
154
61
|
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
):
|
|
161
|
-
logger.error(
|
|
162
|
-
f"You are likely authenticating with a Microsoft Account. \
|
|
163
|
-
This authentication mode only supports Azure Active Directory principal authentication.\
|
|
164
|
-
{e}",
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
raise e
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(
|
|
64
|
+
f"Failed to authenticate with Azure CLI. Have you run 'az login'? Details: {e}"
|
|
65
|
+
)
|
|
66
|
+
return None
|
|
168
67
|
|
|
169
68
|
def authenticate_sp(
|
|
170
69
|
self,
|
|
171
70
|
tenant_id: Optional[str] = None,
|
|
172
71
|
client_id: Optional[str] = None,
|
|
173
72
|
client_secret: Optional[str] = None,
|
|
174
|
-
|
|
73
|
+
subscription_id: Optional[str] = None,
|
|
74
|
+
) -> Optional[Credentials]:
|
|
175
75
|
"""
|
|
176
|
-
Implements authentication
|
|
76
|
+
Implements authentication using a Service Principal with the modern library.
|
|
177
77
|
"""
|
|
178
78
|
try:
|
|
179
|
-
|
|
180
|
-
# Set logging level to error for libraries as otherwise generates a lot of warnings
|
|
181
|
-
logging.getLogger("adal-python").setLevel(logging.ERROR)
|
|
182
|
-
logging.getLogger("msrest").setLevel(logging.ERROR)
|
|
183
|
-
logging.getLogger("msrestazure.azure_active_directory").setLevel(
|
|
184
|
-
logging.ERROR,
|
|
185
|
-
)
|
|
186
|
-
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
|
187
|
-
logging.getLogger(
|
|
188
|
-
"azure.core.pipeline.policies.http_logging_policy",
|
|
189
|
-
).setLevel(logging.ERROR)
|
|
190
|
-
|
|
191
|
-
arm_credentials = ClientSecretCredential(
|
|
192
|
-
client_id=client_id,
|
|
193
|
-
client_secret=client_secret,
|
|
194
|
-
tenant_id=tenant_id,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
aad_graph_credentials = ClientSecretCredential(
|
|
79
|
+
credential = ClientSecretCredential(
|
|
198
80
|
client_id=client_id,
|
|
199
81
|
client_secret=client_secret,
|
|
200
82
|
tenant_id=tenant_id,
|
|
201
|
-
resource="https://graph.windows.net",
|
|
202
83
|
)
|
|
203
|
-
|
|
204
84
|
return Credentials(
|
|
205
|
-
|
|
206
|
-
aad_graph_credentials,
|
|
85
|
+
credential=credential,
|
|
207
86
|
tenant_id=tenant_id,
|
|
208
|
-
|
|
87
|
+
subscription_id=subscription_id,
|
|
209
88
|
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
{e}",
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
raise e
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(
|
|
91
|
+
(
|
|
92
|
+
"Failed to authenticate with Service Principal. "
|
|
93
|
+
"Please ensure the tenant ID, client ID, and client secret are correct. Details: %s"
|
|
94
|
+
),
|
|
95
|
+
e,
|
|
96
|
+
)
|
|
97
|
+
return None
|
|
@@ -2,17 +2,54 @@ import asyncio
|
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
4
|
import neo4j
|
|
5
|
+
from azure.identity import ClientSecretCredential
|
|
6
|
+
from msgraph import GraphServiceClient
|
|
5
7
|
|
|
6
8
|
from cartography.config import Config
|
|
7
9
|
from cartography.intel.entra.applications import sync_entra_applications
|
|
8
10
|
from cartography.intel.entra.groups import sync_entra_groups
|
|
9
11
|
from cartography.intel.entra.ou import sync_entra_ous
|
|
12
|
+
from cartography.intel.entra.users import get_tenant
|
|
13
|
+
from cartography.intel.entra.users import load_tenant
|
|
10
14
|
from cartography.intel.entra.users import sync_entra_users
|
|
15
|
+
from cartography.intel.entra.users import transform_tenant
|
|
11
16
|
from cartography.util import timeit
|
|
12
17
|
|
|
13
18
|
logger = logging.getLogger(__name__)
|
|
14
19
|
|
|
15
20
|
|
|
21
|
+
@timeit
|
|
22
|
+
async def sync_tenant(
|
|
23
|
+
neo4j_session: neo4j.Session,
|
|
24
|
+
tenant_id: str,
|
|
25
|
+
client_id: str,
|
|
26
|
+
client_secret: str,
|
|
27
|
+
update_tag: int,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Sync tenant information as a prerequisite for all other Entra resource syncs.
|
|
31
|
+
|
|
32
|
+
:param neo4j_session: Neo4j session
|
|
33
|
+
:param tenant_id: Entra tenant ID
|
|
34
|
+
:param client_id: Azure application client ID
|
|
35
|
+
:param client_secret: Azure application client secret
|
|
36
|
+
:param update_tag: Update tag for tracking data freshness
|
|
37
|
+
"""
|
|
38
|
+
credential = ClientSecretCredential(
|
|
39
|
+
tenant_id=tenant_id,
|
|
40
|
+
client_id=client_id,
|
|
41
|
+
client_secret=client_secret,
|
|
42
|
+
)
|
|
43
|
+
client = GraphServiceClient(
|
|
44
|
+
credential, scopes=["https://graph.microsoft.com/.default"]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Fetch tenant and load it
|
|
48
|
+
tenant = await get_tenant(client)
|
|
49
|
+
transformed_tenant = transform_tenant(tenant, tenant_id)
|
|
50
|
+
load_tenant(neo4j_session, transformed_tenant, update_tag)
|
|
51
|
+
|
|
52
|
+
|
|
16
53
|
@timeit
|
|
17
54
|
def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
18
55
|
"""
|
|
@@ -39,6 +76,15 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
39
76
|
}
|
|
40
77
|
|
|
41
78
|
async def main() -> None:
|
|
79
|
+
# Load tenant first as a prerequisite for all resource syncs
|
|
80
|
+
await sync_tenant(
|
|
81
|
+
neo4j_session,
|
|
82
|
+
config.entra_tenant_id,
|
|
83
|
+
config.entra_client_id,
|
|
84
|
+
config.entra_client_secret,
|
|
85
|
+
config.update_tag,
|
|
86
|
+
)
|
|
87
|
+
|
|
42
88
|
# Run user sync
|
|
43
89
|
await sync_entra_users(
|
|
44
90
|
neo4j_session,
|
|
@@ -79,5 +125,5 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
79
125
|
common_job_parameters,
|
|
80
126
|
)
|
|
81
127
|
|
|
82
|
-
# Execute
|
|
128
|
+
# Execute syncs in sequence
|
|
83
129
|
asyncio.run(main())
|