cartography 0.114.0__py3-none-any.whl → 0.115.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.

Files changed (39) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +2 -2
  3. cartography/client/core/tx.py +11 -0
  4. cartography/intel/aws/config.py +7 -3
  5. cartography/intel/aws/ecr.py +9 -9
  6. cartography/intel/aws/identitycenter.py +240 -13
  7. cartography/intel/aws/lambda_function.py +69 -2
  8. cartography/intel/aws/organizations.py +3 -1
  9. cartography/intel/aws/permission_relationships.py +3 -1
  10. cartography/intel/aws/redshift.py +9 -4
  11. cartography/intel/aws/route53.py +53 -3
  12. cartography/intel/aws/securityhub.py +3 -1
  13. cartography/intel/azure/__init__.py +8 -0
  14. cartography/intel/azure/logic_apps.py +101 -0
  15. cartography/intel/create_indexes.py +2 -1
  16. cartography/intel/dns.py +5 -2
  17. cartography/intel/gcp/dns.py +2 -1
  18. cartography/intel/github/repos.py +3 -6
  19. cartography/intel/gsuite/api.py +17 -4
  20. cartography/intel/okta/applications.py +9 -4
  21. cartography/intel/okta/awssaml.py +5 -2
  22. cartography/intel/okta/factors.py +3 -1
  23. cartography/intel/okta/groups.py +5 -2
  24. cartography/intel/okta/organization.py +3 -1
  25. cartography/intel/okta/origins.py +3 -1
  26. cartography/intel/okta/roles.py +5 -2
  27. cartography/intel/okta/users.py +3 -1
  28. cartography/models/aws/identitycenter/awspermissionset.py +24 -1
  29. cartography/models/aws/identitycenter/awssogroup.py +70 -0
  30. cartography/models/aws/identitycenter/awsssouser.py +37 -1
  31. cartography/models/aws/lambda_function/lambda_function.py +2 -0
  32. cartography/models/azure/logic_apps.py +56 -0
  33. cartography/models/entra/user.py +18 -0
  34. {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/METADATA +3 -2
  35. {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/RECORD +39 -36
  36. {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/WHEEL +0 -0
  37. {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/entry_points.txt +0 -0
  38. {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/licenses/LICENSE +0 -0
  39. {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/top_level.txt +0 -0
@@ -24,6 +24,7 @@ DnsData = namedtuple(
24
24
  [
25
25
  "zones",
26
26
  "a_records",
27
+ "aaaa_records",
27
28
  "alias_records",
28
29
  "cname_records",
29
30
  "ns_records",
@@ -73,7 +74,7 @@ def get_zones(
73
74
  def transform_record_set(
74
75
  record_set: dict[str, Any], zone_id: str, name: str
75
76
  ) -> dict[str, Any] | None:
76
- # process CNAME, ALIAS and A records
77
+ # process CNAME, ALIAS, A, and AAAA records
77
78
  if record_set["Type"] == "CNAME":
78
79
  if "AliasTarget" in record_set:
79
80
  # this is a weighted CNAME record
@@ -127,6 +128,31 @@ def transform_record_set(
127
128
  "value": value,
128
129
  "id": _create_dns_record_id(zone_id, name, "A"),
129
130
  }
131
+ elif record_set["Type"] == "AAAA":
132
+ if "AliasTarget" in record_set:
133
+ # AAAA alias records follow the same pattern as A aliases but map to IPv6 targets
134
+ value = record_set["AliasTarget"]["DNSName"]
135
+ if value.endswith("."):
136
+ value = value[:-1]
137
+ return {
138
+ "name": name,
139
+ "type": "ALIAS",
140
+ "zoneid": zone_id,
141
+ "value": value,
142
+ "id": _create_dns_record_id(zone_id, name, "ALIAS_AAAA"),
143
+ }
144
+ else:
145
+ ip_addresses = [record["Value"] for record in record_set["ResourceRecords"]]
146
+ value = ",".join(ip_addresses)
147
+
148
+ return {
149
+ "name": name,
150
+ "type": "AAAA",
151
+ "zoneid": zone_id,
152
+ "ip_addresses": ip_addresses,
153
+ "value": value,
154
+ "id": _create_dns_record_id(zone_id, name, "AAAA"),
155
+ }
130
156
  # This should never happen since we only call this for A and CNAME records,
131
157
  # but we'll log it and return None.
132
158
  logger.warning(f"Unsupported record type: {record_set['Type']}")
@@ -179,10 +205,11 @@ def transform_all_dns_data(
179
205
  ) -> DnsData:
180
206
  """
181
207
  Transform all DNS data into flat lists for loading.
182
- Returns: (zones, a_records, alias_records, cname_records, ns_records)
208
+ Returns: (zones, a_records, aaaa_records, alias_records, cname_records, ns_records)
183
209
  """
184
210
  transformed_zones = []
185
211
  all_a_records = []
212
+ all_aaaa_records = []
186
213
  all_alias_records = []
187
214
  all_cname_records = []
188
215
  all_ns_records = []
@@ -196,7 +223,7 @@ def transform_all_dns_data(
196
223
  zone_name = parsed_zone["name"]
197
224
 
198
225
  for rs in zone_record_sets:
199
- if rs["Type"] == "A" or rs["Type"] == "CNAME":
226
+ if rs["Type"] in {"A", "AAAA", "CNAME"}:
200
227
  transformed_rs = transform_record_set(
201
228
  rs,
202
229
  zone_id,
@@ -209,6 +236,8 @@ def transform_all_dns_data(
209
236
  all_a_records.append(transformed_rs)
210
237
  # TODO consider creating IPs as a first-class node from here.
211
238
  # Right now we just match on them from the A record.
239
+ elif transformed_rs["type"] == "AAAA":
240
+ all_aaaa_records.append(transformed_rs)
212
241
  elif transformed_rs["type"] == "ALIAS":
213
242
  all_alias_records.append(transformed_rs)
214
243
  elif transformed_rs["type"] == "CNAME":
@@ -232,6 +261,7 @@ def transform_all_dns_data(
232
261
  return DnsData(
233
262
  zones=transformed_zones,
234
263
  a_records=all_a_records,
264
+ aaaa_records=all_aaaa_records,
235
265
  alias_records=all_alias_records,
236
266
  cname_records=all_cname_records,
237
267
  ns_records=all_ns_records,
@@ -244,6 +274,7 @@ def _load_dns_details_flat(
244
274
  neo4j_session: neo4j.Session,
245
275
  zones: list[dict[str, Any]],
246
276
  a_records: list[dict[str, Any]],
277
+ aaaa_records: list[dict[str, Any]],
247
278
  alias_records: list[dict[str, Any]],
248
279
  cname_records: list[dict[str, Any]],
249
280
  ns_records: list[dict[str, Any]],
@@ -253,6 +284,7 @@ def _load_dns_details_flat(
253
284
  ) -> None:
254
285
  load_zones(neo4j_session, zones, current_aws_id, update_tag)
255
286
  load_a_records(neo4j_session, a_records, update_tag, current_aws_id)
287
+ load_aaaa_records(neo4j_session, aaaa_records, update_tag, current_aws_id)
256
288
  load_alias_records(neo4j_session, alias_records, update_tag, current_aws_id)
257
289
  load_cname_records(neo4j_session, cname_records, update_tag, current_aws_id)
258
290
  load_name_servers(neo4j_session, name_servers, update_tag, current_aws_id)
@@ -274,6 +306,7 @@ def load_dns_details(
274
306
  neo4j_session,
275
307
  transformed_data.zones,
276
308
  transformed_data.a_records,
309
+ transformed_data.aaaa_records,
277
310
  transformed_data.alias_records,
278
311
  transformed_data.cname_records,
279
312
  transformed_data.ns_records,
@@ -299,6 +332,22 @@ def load_a_records(
299
332
  )
300
333
 
301
334
 
335
+ @timeit
336
+ def load_aaaa_records(
337
+ neo4j_session: neo4j.Session,
338
+ records: list[dict[str, Any]],
339
+ update_tag: int,
340
+ current_aws_id: str,
341
+ ) -> None:
342
+ load(
343
+ neo4j_session,
344
+ AWSDNSRecordSchema(),
345
+ records,
346
+ lastupdated=update_tag,
347
+ AWS_ID=current_aws_id,
348
+ )
349
+
350
+
302
351
  @timeit
303
352
  def load_alias_records(
304
353
  neo4j_session: neo4j.Session,
@@ -468,6 +517,7 @@ def sync(
468
517
  neo4j_session,
469
518
  transformed_data.zones,
470
519
  transformed_data.a_records,
520
+ transformed_data.aaaa_records,
471
521
  transformed_data.alias_records,
472
522
  transformed_data.cname_records,
473
523
  transformed_data.ns_records,
@@ -6,6 +6,7 @@ import boto3
6
6
  import neo4j
7
7
  from dateutil import parser
8
8
 
9
+ from cartography.client.core.tx import run_write_query
9
10
  from cartography.util import run_cleanup_job
10
11
  from cartography.util import timeit
11
12
 
@@ -50,7 +51,8 @@ def load_hub(
50
51
  ON CREATE SET r.firstseen = timestamp()
51
52
  SET r.lastupdated = $aws_update_tag
52
53
  """
53
- neo4j_session.run(
54
+ run_write_query(
55
+ neo4j_session,
54
56
  ingest_hub,
55
57
  Hub=data,
56
58
  AWS_ACCOUNT_ID=current_aws_account_id,
@@ -11,6 +11,7 @@ from . import app_service
11
11
  from . import compute
12
12
  from . import cosmosdb
13
13
  from . import functions
14
+ from . import logic_apps
14
15
  from . import sql
15
16
  from . import storage
16
17
  from . import subscription
@@ -56,6 +57,13 @@ def _sync_one_subscription(
56
57
  update_tag,
57
58
  common_job_parameters,
58
59
  )
60
+ logic_apps.sync(
61
+ neo4j_session,
62
+ credentials,
63
+ subscription_id,
64
+ update_tag,
65
+ common_job_parameters,
66
+ )
59
67
  sql.sync(
60
68
  neo4j_session,
61
69
  credentials.credential,
@@ -0,0 +1,101 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ from azure.core.exceptions import ClientAuthenticationError
6
+ from azure.core.exceptions import HttpResponseError
7
+ from azure.mgmt.logic import LogicManagementClient
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.models.azure.logic_apps import AzureLogicAppSchema
12
+ from cartography.util import timeit
13
+
14
+ from .util.credentials import Credentials
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ def get_logic_apps(credentials: Credentials, subscription_id: str) -> list[dict]:
21
+ """
22
+ Get a list of Logic Apps from the given Azure subscription.
23
+ """
24
+ try:
25
+ client = LogicManagementClient(credentials.credential, subscription_id)
26
+ # NOTE: The resource for a Logic App is called a "Workflow" in the SDK.
27
+ return [w.as_dict() for w in client.workflows.list_by_subscription()]
28
+ except (ClientAuthenticationError, HttpResponseError) as e:
29
+ logger.warning(
30
+ f"Failed to get logic apps for subscription {subscription_id}: {str(e)}"
31
+ )
32
+ return []
33
+
34
+
35
+ def transform_logic_apps(logic_apps_response: list[dict]) -> list[dict]:
36
+ """
37
+ Transform the raw API response to the dictionary structure that the model expects.
38
+ """
39
+ transformed_apps: list[dict[str, Any]] = []
40
+ for app in logic_apps_response:
41
+ transformed_app = {
42
+ "id": app.get("id"),
43
+ "name": app.get("name"),
44
+ "location": app.get("location"),
45
+ "state": app.get("properties", {}).get("state"),
46
+ "created_time": app.get("properties", {}).get("created_time"),
47
+ "changed_time": app.get("properties", {}).get("changed_time"),
48
+ "version": app.get("properties", {}).get("version"),
49
+ "access_endpoint": app.get("properties", {}).get("access_endpoint"),
50
+ }
51
+ transformed_apps.append(transformed_app)
52
+ return transformed_apps
53
+
54
+
55
+ @timeit
56
+ def load_logic_apps(
57
+ neo4j_session: neo4j.Session,
58
+ data: list[dict[str, Any]],
59
+ subscription_id: str,
60
+ update_tag: int,
61
+ ) -> None:
62
+ """
63
+ Load the transformed Azure Logic App data to Neo4j.
64
+ """
65
+ load(
66
+ neo4j_session,
67
+ AzureLogicAppSchema(),
68
+ data,
69
+ lastupdated=update_tag,
70
+ AZURE_SUBSCRIPTION_ID=subscription_id,
71
+ )
72
+
73
+
74
+ @timeit
75
+ def cleanup_logic_apps(
76
+ neo4j_session: neo4j.Session, common_job_parameters: dict
77
+ ) -> None:
78
+ """
79
+ Run the cleanup job for Azure Logic Apps.
80
+ """
81
+ GraphJob.from_node_schema(AzureLogicAppSchema(), common_job_parameters).run(
82
+ neo4j_session
83
+ )
84
+
85
+
86
+ @timeit
87
+ def sync(
88
+ neo4j_session: neo4j.Session,
89
+ credentials: Credentials,
90
+ subscription_id: str,
91
+ update_tag: int,
92
+ common_job_parameters: dict,
93
+ ) -> None:
94
+ """
95
+ The main sync function for Azure Logic Apps.
96
+ """
97
+ logger.info(f"Syncing Azure Logic Apps for subscription {subscription_id}.")
98
+ raw_apps = get_logic_apps(credentials, subscription_id)
99
+ transformed_apps = transform_logic_apps(raw_apps)
100
+ load_logic_apps(neo4j_session, transformed_apps, subscription_id, update_tag)
101
+ cleanup_logic_apps(neo4j_session, common_job_parameters)
@@ -3,6 +3,7 @@ from typing import List
3
3
 
4
4
  import neo4j
5
5
 
6
+ from cartography.client.core.tx import run_write_query
6
7
  from cartography.config import Config
7
8
  from cartography.util import load_resource_binary
8
9
 
@@ -23,4 +24,4 @@ def run(neo4j_session: neo4j.Session, config: Config) -> None:
23
24
  logger.info("Creating indexes for cartography node types.")
24
25
  for statement in get_index_statements():
25
26
  logger.debug("Executing statement: %s", statement)
26
- neo4j_session.run(statement)
27
+ run_write_query(neo4j_session, statement)
cartography/intel/dns.py CHANGED
@@ -8,6 +8,7 @@ import dns.rdatatype
8
8
  import dns.resolver
9
9
  import neo4j
10
10
 
11
+ from cartography.client.core.tx import run_write_query
11
12
  from cartography.util import timeit
12
13
 
13
14
  logger = logging.getLogger(__name__)
@@ -104,7 +105,8 @@ def _link_ip_to_A_record(
104
105
  SET r.lastupdated = $update_tag
105
106
  """
106
107
 
107
- neo4j_session.run(
108
+ run_write_query(
109
+ neo4j_session,
108
110
  ingest,
109
111
  ParentId=parent_record,
110
112
  IP_LIST=ip_list,
@@ -151,7 +153,8 @@ def ingest_dns_record(
151
153
 
152
154
  record_id = f"{name}+{type}"
153
155
 
154
- neo4j_session.run(
156
+ run_write_query(
157
+ neo4j_session,
155
158
  template.safe_substitute(
156
159
  record_label=record_label,
157
160
  dns_node_additional_label=dns_node_additional_label,
@@ -116,7 +116,8 @@ def transform_dns_rrs(dns_rrs: List[Dict]) -> List[Dict]:
116
116
  for r in dns_rrs:
117
117
  records.append(
118
118
  {
119
- "id": r["name"],
119
+ # Compose a unique ID to avoid collisions across types and zones
120
+ "id": f"{r['name']}|{r.get('type')}|{r.get('zone')}",
120
121
  "name": r["name"],
121
122
  "type": r.get("type"),
122
123
  "ttl": r.get("ttl"),
@@ -159,8 +159,6 @@ def _get_repo_collaborators_inner_func(
159
159
  token: str,
160
160
  repo_raw_data: list[dict[str, Any]],
161
161
  affiliation: str,
162
- collab_users: list[dict[str, Any]],
163
- collab_permission: list[str],
164
162
  ) -> dict[str, list[UserAffiliationAndRepoPermission]]:
165
163
  result: dict[str, list[UserAffiliationAndRepoPermission]] = {}
166
164
 
@@ -194,6 +192,9 @@ def _get_repo_collaborators_inner_func(
194
192
  affiliation,
195
193
  )
196
194
 
195
+ collab_users: List[dict[str, Any]] = []
196
+ collab_permission: List[str] = []
197
+
197
198
  # nodes and edges are expected to always be present given that we only call for them if totalCount is > 0
198
199
  # however sometimes GitHub returns None, as in issue 1334 and 1404.
199
200
  for collab in collaborators.nodes or []:
@@ -230,8 +231,6 @@ def _get_repo_collaborators_for_multiple_repos(
230
231
  logger.info(
231
232
  f'Retrieving repo collaborators for affiliation "{affiliation}" on org "{org}".',
232
233
  )
233
- collab_users: List[dict[str, Any]] = []
234
- collab_permission: List[str] = []
235
234
 
236
235
  result: dict[str, list[UserAffiliationAndRepoPermission]] = retries_with_backoff(
237
236
  _get_repo_collaborators_inner_func,
@@ -244,8 +243,6 @@ def _get_repo_collaborators_for_multiple_repos(
244
243
  token=token,
245
244
  repo_raw_data=repo_raw_data,
246
245
  affiliation=affiliation,
247
- collab_users=collab_users,
248
- collab_permission=collab_permission,
249
246
  )
250
247
  return result
251
248
 
@@ -6,6 +6,7 @@ import neo4j
6
6
  from googleapiclient.discovery import Resource
7
7
  from googleapiclient.errors import HttpError
8
8
 
9
+ from cartography.client.core.tx import run_write_query
9
10
  from cartography.util import run_cleanup_job
10
11
  from cartography.util import timeit
11
12
 
@@ -171,7 +172,12 @@ def load_gsuite_groups(
171
172
  g.lastupdated = $UpdateTag
172
173
  """
173
174
  logger.info(f"Ingesting {len(groups)} gsuite groups")
174
- neo4j_session.run(ingestion_qry, GroupData=groups, UpdateTag=gsuite_update_tag)
175
+ run_write_query(
176
+ neo4j_session,
177
+ ingestion_qry,
178
+ GroupData=groups,
179
+ UpdateTag=gsuite_update_tag,
180
+ )
175
181
 
176
182
 
177
183
  @timeit
@@ -215,7 +221,12 @@ def load_gsuite_users(
215
221
  u.lastupdated = $UpdateTag
216
222
  """
217
223
  logger.info(f"Ingesting {len(users)} gsuite users")
218
- neo4j_session.run(ingestion_qry, UserData=users, UpdateTag=gsuite_update_tag)
224
+ run_write_query(
225
+ neo4j_session,
226
+ ingestion_qry,
227
+ UserData=users,
228
+ UpdateTag=gsuite_update_tag,
229
+ )
219
230
 
220
231
 
221
232
  @timeit
@@ -234,7 +245,8 @@ def load_gsuite_members(
234
245
  SET
235
246
  r.lastupdated = $UpdateTag
236
247
  """
237
- neo4j_session.run(
248
+ run_write_query(
249
+ neo4j_session,
238
250
  ingestion_qry,
239
251
  MemberData=members,
240
252
  GroupID=group.get("id"),
@@ -249,7 +261,8 @@ def load_gsuite_members(
249
261
  SET
250
262
  r.lastupdated = $UpdateTag
251
263
  """
252
- neo4j_session.run(
264
+ run_write_query(
265
+ neo4j_session,
253
266
  membership_qry,
254
267
  MemberData=members,
255
268
  GroupID=group.get("id"),
@@ -10,6 +10,7 @@ import neo4j
10
10
  from okta.framework.ApiClient import ApiClient
11
11
  from okta.framework.OktaError import OktaError
12
12
 
13
+ from cartography.client.core.tx import run_write_query
13
14
  from cartography.intel.okta.utils import check_rate_limit
14
15
  from cartography.intel.okta.utils import create_api_client
15
16
  from cartography.intel.okta.utils import is_last_page
@@ -293,7 +294,8 @@ def _load_okta_applications(
293
294
  SET org_r.lastupdated = $okta_update_tag
294
295
  """
295
296
 
296
- neo4j_session.run(
297
+ run_write_query(
298
+ neo4j_session,
297
299
  ingest_statement,
298
300
  ORG_ID=okta_org_id,
299
301
  APP_LIST=app_list,
@@ -327,7 +329,8 @@ def _load_application_user(
327
329
  SET r.lastupdated = $okta_update_tag
328
330
  """
329
331
 
330
- neo4j_session.run(
332
+ run_write_query(
333
+ neo4j_session,
331
334
  ingest,
332
335
  APP_ID=app_id,
333
336
  USER_LIST=user_list,
@@ -361,7 +364,8 @@ def _load_application_group(
361
364
  SET r.lastupdated = $okta_update_tag
362
365
  """
363
366
 
364
- neo4j_session.run(
367
+ run_write_query(
368
+ neo4j_session,
365
369
  ingest,
366
370
  APP_ID=app_id,
367
371
  GROUP_LIST=group_list,
@@ -400,7 +404,8 @@ def _load_application_reply_urls(
400
404
  SET r.lastupdated = $okta_update_tag
401
405
  """
402
406
 
403
- neo4j_session.run(
407
+ run_write_query(
408
+ neo4j_session,
404
409
  ingest,
405
410
  APP_ID=app_id,
406
411
  URL_LIST=reply_urls,
@@ -10,6 +10,7 @@ import neo4j
10
10
 
11
11
  from cartography.client.core.tx import read_list_of_dicts_tx
12
12
  from cartography.client.core.tx import read_single_value_tx
13
+ from cartography.client.core.tx import run_write_query
13
14
  from cartography.util import timeit
14
15
 
15
16
  AccountRole = namedtuple("AccountRole", ["account_id", "role_name"])
@@ -116,7 +117,8 @@ def _load_okta_group_to_aws_roles(
116
117
  SET r.lastupdated = $okta_update_tag
117
118
  """
118
119
 
119
- neo4j_session.run(
120
+ run_write_query(
121
+ neo4j_session,
120
122
  ingest_statement,
121
123
  GROUP_TO_ROLE=group_to_role,
122
124
  okta_update_tag=okta_update_tag,
@@ -140,7 +142,8 @@ def _load_human_can_assume_role(
140
142
  SET r.lastupdated = $okta_update_tag
141
143
  """
142
144
 
143
- neo4j_session.run(
145
+ run_write_query(
146
+ neo4j_session,
144
147
  ingest_statement,
145
148
  okta_update_tag=okta_update_tag,
146
149
  )
@@ -8,6 +8,7 @@ from okta import FactorsClient
8
8
  from okta.framework.OktaError import OktaError
9
9
  from okta.models.factor.Factor import Factor
10
10
 
11
+ from cartography.client.core.tx import run_write_query
11
12
  from cartography.intel.okta.sync_state import OktaSyncState
12
13
  from cartography.util import timeit
13
14
 
@@ -130,7 +131,8 @@ def _load_user_factors(
130
131
  SET r.lastupdated = $okta_update_tag
131
132
  """
132
133
 
133
- neo4j_session.run(
134
+ run_write_query(
135
+ neo4j_session,
134
136
  ingest,
135
137
  USER_ID=user_id,
136
138
  FACTOR_LIST=factors,
@@ -11,6 +11,7 @@ from okta.framework.OktaError import OktaError
11
11
  from okta.framework.PagedResults import PagedResults
12
12
  from okta.models.usergroup import UserGroup
13
13
 
14
+ from cartography.client.core.tx import run_write_query
14
15
  from cartography.intel.okta.sync_state import OktaSyncState
15
16
  from cartography.intel.okta.utils import check_rate_limit
16
17
  from cartography.intel.okta.utils import create_api_client
@@ -204,7 +205,8 @@ def _load_okta_groups(
204
205
  SET org_r.lastupdated = $okta_update_tag
205
206
  """
206
207
 
207
- neo4j_session.run(
208
+ run_write_query(
209
+ neo4j_session,
208
210
  ingest_statement,
209
211
  ORG_ID=okta_org_id,
210
212
  GROUP_LIST=group_list,
@@ -251,7 +253,8 @@ def load_okta_group_members(
251
253
  SET r.lastupdated = $okta_update_tag
252
254
  """
253
255
  logging.info(f"Loading {len(member_list)} members of group {group_id}")
254
- neo4j_session.run(
256
+ run_write_query(
257
+ neo4j_session,
255
258
  ingest,
256
259
  GROUP_ID=group_id,
257
260
  MEMBER_LIST=member_list,
@@ -3,6 +3,7 @@ import logging
3
3
 
4
4
  import neo4j
5
5
 
6
+ from cartography.client.core.tx import run_write_query
6
7
  from cartography.util import timeit
7
8
 
8
9
  logger = logging.getLogger(__name__)
@@ -27,7 +28,8 @@ def create_okta_organization(
27
28
  SET org.lastupdated = $okta_update_tag
28
29
  """
29
30
 
30
- neo4j_session.run(
31
+ run_write_query(
32
+ neo4j_session,
31
33
  ingest,
32
34
  ORG_NAME=organization,
33
35
  okta_update_tag=okta_update_tag,
@@ -7,6 +7,7 @@ from typing import List
7
7
  import neo4j
8
8
  from okta.framework.ApiClient import ApiClient
9
9
 
10
+ from cartography.client.core.tx import run_write_query
10
11
  from cartography.intel.okta.utils import create_api_client
11
12
  from cartography.util import timeit
12
13
 
@@ -96,7 +97,8 @@ def _load_trusted_origins(
96
97
  SET r.lastupdated = $okta_update_tag
97
98
  """
98
99
 
99
- neo4j_session.run(
100
+ run_write_query(
101
+ neo4j_session,
100
102
  ingest,
101
103
  ORG_ID=okta_org_id,
102
104
  TRUSTED_LIST=trusted_list,
@@ -7,6 +7,7 @@ from typing import List
7
7
  import neo4j
8
8
  from okta.framework.ApiClient import ApiClient
9
9
 
10
+ from cartography.client.core.tx import run_write_query
10
11
  from cartography.intel.okta.sync_state import OktaSyncState
11
12
  from cartography.intel.okta.utils import check_rate_limit
12
13
  from cartography.intel.okta.utils import create_api_client
@@ -117,7 +118,8 @@ def _load_user_role(
117
118
  SET r2.lastupdated = $okta_update_tag
118
119
  """
119
120
 
120
- neo4j_session.run(
121
+ run_write_query(
122
+ neo4j_session,
121
123
  ingest,
122
124
  USER_ID=user_id,
123
125
  ROLES_DATA=roles_data,
@@ -149,7 +151,8 @@ def _load_group_role(
149
151
  SET r2.lastupdated = $okta_update_tag
150
152
  """
151
153
 
152
- neo4j_session.run(
154
+ run_write_query(
155
+ neo4j_session,
153
156
  ingest,
154
157
  GROUP_ID=group_id,
155
158
  ROLES_DATA=roles_data,
@@ -8,6 +8,7 @@ import neo4j
8
8
  from okta import UsersClient
9
9
  from okta.models.user import User
10
10
 
11
+ from cartography.client.core.tx import run_write_query
11
12
  from cartography.intel.okta.sync_state import OktaSyncState
12
13
  from cartography.intel.okta.utils import check_rate_limit
13
14
  from cartography.util import timeit
@@ -174,7 +175,8 @@ def _load_okta_users(
174
175
  SET h.lastupdated = $okta_update_tag
175
176
  """
176
177
 
177
- neo4j_session.run(
178
+ run_write_query(
179
+ neo4j_session,
178
180
  ingest_statement,
179
181
  ORG_ID=okta_org_id,
180
182
  USER_LIST=user_list,
@@ -82,7 +82,7 @@ class AWSPermissionSetToAWSAccountRel(CartographyRelSchema):
82
82
  @dataclass(frozen=True)
83
83
  class RoleAssignmentAllowedByRelProperties(CartographyRelProperties):
84
84
  """
85
- Properties for the ALLOWED_BY relationship between AWSRole and AWSSSOUser.
85
+ Properties for the ALLOWED_BY relationship between AWSRole and AWSSSO principals.
86
86
  """
87
87
 
88
88
  # Mandatory fields for MatchLinks
@@ -121,6 +121,29 @@ class RoleAssignmentAllowedByMatchLink(CartographyRelSchema):
121
121
  )
122
122
 
123
123
 
124
+ @dataclass(frozen=True)
125
+ class RoleAssignmentAllowedByGroupMatchLink(CartographyRelSchema):
126
+ """
127
+ MatchLink schema for ALLOWED_BY relationships from group role assignments.
128
+ Creates relationships like: (AWSRole)-[:ALLOWED_BY]->(AWSSSOGroup)
129
+ """
130
+
131
+ source_node_label: str = "AWSRole"
132
+ source_node_matcher: SourceNodeMatcher = make_source_node_matcher(
133
+ {"arn": PropertyRef("RoleArn")},
134
+ )
135
+
136
+ target_node_label: str = "AWSSSOGroup"
137
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
138
+ {"id": PropertyRef("GroupId")},
139
+ )
140
+ direction: LinkDirection = LinkDirection.OUTWARD
141
+ rel_label: str = "ALLOWED_BY"
142
+ properties: RoleAssignmentAllowedByRelProperties = (
143
+ RoleAssignmentAllowedByRelProperties()
144
+ )
145
+
146
+
124
147
  @dataclass(frozen=True)
125
148
  class AWSPermissionSetSchema(CartographyNodeSchema):
126
149
  label: str = "AWSPermissionSet"