cartography 0.113.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 (96) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +10 -2
  3. cartography/client/core/tx.py +11 -0
  4. cartography/config.py +4 -0
  5. cartography/data/indexes.cypher +0 -27
  6. cartography/intel/aws/config.py +7 -3
  7. cartography/intel/aws/ecr.py +9 -9
  8. cartography/intel/aws/iam.py +741 -492
  9. cartography/intel/aws/identitycenter.py +240 -13
  10. cartography/intel/aws/lambda_function.py +69 -2
  11. cartography/intel/aws/organizations.py +10 -9
  12. cartography/intel/aws/permission_relationships.py +7 -17
  13. cartography/intel/aws/redshift.py +9 -4
  14. cartography/intel/aws/route53.py +53 -3
  15. cartography/intel/aws/securityhub.py +3 -1
  16. cartography/intel/azure/__init__.py +24 -0
  17. cartography/intel/azure/app_service.py +105 -0
  18. cartography/intel/azure/functions.py +124 -0
  19. cartography/intel/azure/logic_apps.py +101 -0
  20. cartography/intel/create_indexes.py +2 -1
  21. cartography/intel/dns.py +5 -2
  22. cartography/intel/entra/__init__.py +31 -0
  23. cartography/intel/entra/app_role_assignments.py +277 -0
  24. cartography/intel/entra/applications.py +4 -238
  25. cartography/intel/entra/federation/__init__.py +0 -0
  26. cartography/intel/entra/federation/aws_identity_center.py +77 -0
  27. cartography/intel/entra/service_principals.py +217 -0
  28. cartography/intel/gcp/__init__.py +136 -440
  29. cartography/intel/gcp/clients.py +65 -0
  30. cartography/intel/gcp/compute.py +18 -44
  31. cartography/intel/gcp/crm/__init__.py +0 -0
  32. cartography/intel/gcp/crm/folders.py +108 -0
  33. cartography/intel/gcp/crm/orgs.py +65 -0
  34. cartography/intel/gcp/crm/projects.py +109 -0
  35. cartography/intel/gcp/dns.py +2 -1
  36. cartography/intel/gcp/gke.py +72 -113
  37. cartography/intel/github/__init__.py +41 -0
  38. cartography/intel/github/commits.py +423 -0
  39. cartography/intel/github/repos.py +76 -45
  40. cartography/intel/gsuite/api.py +17 -4
  41. cartography/intel/okta/applications.py +9 -4
  42. cartography/intel/okta/awssaml.py +5 -2
  43. cartography/intel/okta/factors.py +3 -1
  44. cartography/intel/okta/groups.py +5 -2
  45. cartography/intel/okta/organization.py +3 -1
  46. cartography/intel/okta/origins.py +3 -1
  47. cartography/intel/okta/roles.py +5 -2
  48. cartography/intel/okta/users.py +3 -1
  49. cartography/models/aws/iam/access_key.py +103 -0
  50. cartography/models/aws/iam/account_role.py +24 -0
  51. cartography/models/aws/iam/federated_principal.py +60 -0
  52. cartography/models/aws/iam/group.py +60 -0
  53. cartography/models/aws/iam/group_membership.py +26 -0
  54. cartography/models/aws/iam/inline_policy.py +78 -0
  55. cartography/models/aws/iam/managed_policy.py +51 -0
  56. cartography/models/aws/iam/policy_statement.py +57 -0
  57. cartography/models/aws/iam/role.py +83 -0
  58. cartography/models/aws/iam/root_principal.py +52 -0
  59. cartography/models/aws/iam/service_principal.py +30 -0
  60. cartography/models/aws/iam/sts_assumerole_allow.py +38 -0
  61. cartography/models/aws/iam/user.py +54 -0
  62. cartography/models/aws/identitycenter/awspermissionset.py +24 -1
  63. cartography/models/aws/identitycenter/awssogroup.py +70 -0
  64. cartography/models/aws/identitycenter/awsssouser.py +37 -1
  65. cartography/models/aws/lambda_function/lambda_function.py +2 -0
  66. cartography/models/azure/__init__.py +0 -0
  67. cartography/models/azure/app_service.py +59 -0
  68. cartography/models/azure/function_app.py +59 -0
  69. cartography/models/azure/logic_apps.py +56 -0
  70. cartography/models/entra/entra_user_to_aws_sso.py +41 -0
  71. cartography/models/entra/service_principal.py +104 -0
  72. cartography/models/entra/user.py +18 -0
  73. cartography/models/gcp/compute/subnet.py +74 -0
  74. cartography/models/gcp/crm/__init__.py +0 -0
  75. cartography/models/gcp/crm/folders.py +98 -0
  76. cartography/models/gcp/crm/organizations.py +21 -0
  77. cartography/models/gcp/crm/projects.py +100 -0
  78. cartography/models/gcp/gke.py +69 -0
  79. cartography/models/github/commits.py +63 -0
  80. {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/METADATA +8 -5
  81. {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/RECORD +85 -56
  82. cartography/data/jobs/cleanup/aws_import_account_access_key_cleanup.json +0 -17
  83. cartography/data/jobs/cleanup/aws_import_groups_cleanup.json +0 -13
  84. cartography/data/jobs/cleanup/aws_import_principals_cleanup.json +0 -30
  85. cartography/data/jobs/cleanup/aws_import_roles_cleanup.json +0 -13
  86. cartography/data/jobs/cleanup/aws_import_users_cleanup.json +0 -8
  87. cartography/data/jobs/cleanup/gcp_compute_vpc_subnet_cleanup.json +0 -35
  88. cartography/data/jobs/cleanup/gcp_crm_folder_cleanup.json +0 -23
  89. cartography/data/jobs/cleanup/gcp_crm_organization_cleanup.json +0 -17
  90. cartography/data/jobs/cleanup/gcp_crm_project_cleanup.json +0 -23
  91. cartography/data/jobs/cleanup/gcp_gke_cluster_cleanup.json +0 -17
  92. cartography/intel/gcp/crm.py +0 -355
  93. {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/WHEEL +0 -0
  94. {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/entry_points.txt +0 -0
  95. {cartography-0.113.0.dist-info → cartography-0.115.0.dist-info}/licenses/LICENSE +0 -0
  96. {cartography-0.113.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,
@@ -7,8 +7,11 @@ import neo4j
7
7
  from cartography.config import Config
8
8
  from cartography.util import timeit
9
9
 
10
+ from . import app_service
10
11
  from . import compute
11
12
  from . import cosmosdb
13
+ from . import functions
14
+ from . import logic_apps
12
15
  from . import sql
13
16
  from . import storage
14
17
  from . import subscription
@@ -40,6 +43,27 @@ def _sync_one_subscription(
40
43
  update_tag,
41
44
  common_job_parameters,
42
45
  )
46
+ app_service.sync(
47
+ neo4j_session,
48
+ credentials,
49
+ subscription_id,
50
+ update_tag,
51
+ common_job_parameters,
52
+ )
53
+ functions.sync(
54
+ neo4j_session,
55
+ credentials,
56
+ subscription_id,
57
+ update_tag,
58
+ common_job_parameters,
59
+ )
60
+ logic_apps.sync(
61
+ neo4j_session,
62
+ credentials,
63
+ subscription_id,
64
+ update_tag,
65
+ common_job_parameters,
66
+ )
43
67
  sql.sync(
44
68
  neo4j_session,
45
69
  credentials.credential,
@@ -0,0 +1,105 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import neo4j
7
+ from azure.core.exceptions import ClientAuthenticationError
8
+ from azure.core.exceptions import HttpResponseError
9
+ from azure.mgmt.web import WebSiteManagementClient
10
+
11
+ from cartography.client.core.tx import load
12
+ from cartography.graph.job import GraphJob
13
+ from cartography.models.azure.app_service import AzureAppServiceSchema
14
+ from cartography.util import timeit
15
+
16
+ from .util.credentials import Credentials
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @timeit
22
+ def get_app_services(credentials: Credentials, subscription_id: str) -> List[Dict]:
23
+ """
24
+ Get a list of App Services from the given Azure subscription.
25
+ """
26
+ try:
27
+ client = WebSiteManagementClient(credentials.credential, subscription_id)
28
+ # NOTE: This is the same API call as Functions. We get all web apps
29
+ # and then filter them in the transform stage.
30
+ return [app.as_dict() for app in client.web_apps.list()]
31
+ except (ClientAuthenticationError, HttpResponseError) as e:
32
+ logger.warning(
33
+ f"Failed to get app services for subscription {subscription_id}: {str(e)}"
34
+ )
35
+ return []
36
+
37
+
38
+ @timeit
39
+ def transform_app_services(app_services_response: List[Dict]) -> List[Dict]:
40
+ """
41
+ Transform the raw API response to the dictionary structure that the model expects.
42
+ """
43
+ transformed_apps: List[Dict[str, Any]] = []
44
+ for app in app_services_response:
45
+ if "functionapp" not in app.get("kind", ""):
46
+ transformed_app = {
47
+ "id": app.get("id"),
48
+ "name": app.get("name"),
49
+ "kind": app.get("kind"),
50
+ "location": app.get("location"),
51
+ "state": app.get("state"),
52
+ "default_host_name": app.get("default_host_name"),
53
+ "https_only": app.get("https_only"),
54
+ }
55
+ transformed_apps.append(transformed_app)
56
+ return transformed_apps
57
+
58
+
59
+ @timeit
60
+ def load_app_services(
61
+ neo4j_session: neo4j.Session,
62
+ data: List[Dict[str, Any]],
63
+ subscription_id: str,
64
+ update_tag: int,
65
+ ) -> None:
66
+ """
67
+ Load the transformed Azure App Service data to Neo4j.
68
+ """
69
+ load(
70
+ neo4j_session,
71
+ AzureAppServiceSchema(),
72
+ data,
73
+ lastupdated=update_tag,
74
+ AZURE_SUBSCRIPTION_ID=subscription_id,
75
+ )
76
+
77
+
78
+ @timeit
79
+ def cleanup_app_services(
80
+ neo4j_session: neo4j.Session, common_job_parameters: Dict
81
+ ) -> None:
82
+ """
83
+ Run the cleanup job for Azure App Services.
84
+ """
85
+ GraphJob.from_node_schema(AzureAppServiceSchema(), common_job_parameters).run(
86
+ neo4j_session
87
+ )
88
+
89
+
90
+ @timeit
91
+ def sync(
92
+ neo4j_session: neo4j.Session,
93
+ credentials: Credentials,
94
+ subscription_id: str,
95
+ update_tag: int,
96
+ common_job_parameters: Dict,
97
+ ) -> None:
98
+ """
99
+ The main sync function for Azure App Services.
100
+ """
101
+ logger.info(f"Syncing Azure App Services for subscription {subscription_id}.")
102
+ raw_apps = get_app_services(credentials, subscription_id)
103
+ transformed_apps = transform_app_services(raw_apps)
104
+ load_app_services(neo4j_session, transformed_apps, subscription_id, update_tag)
105
+ cleanup_app_services(neo4j_session, common_job_parameters)
@@ -0,0 +1,124 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import neo4j
7
+ from azure.core.exceptions import ClientAuthenticationError
8
+ from azure.core.exceptions import HttpResponseError
9
+ from azure.mgmt.web import WebSiteManagementClient
10
+
11
+ from cartography.client.core.tx import load
12
+ from cartography.graph.job import GraphJob
13
+ from cartography.models.azure.function_app import AzureFunctionAppSchema
14
+ from cartography.util import timeit
15
+
16
+ from .util.credentials import Credentials
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @timeit
22
+ def get_function_apps(credentials: Credentials, subscription_id: str) -> List[Dict]:
23
+ """
24
+ Get a list of Function Apps from the given Azure subscription.
25
+ """
26
+ try:
27
+ client = WebSiteManagementClient(credentials.credential, subscription_id)
28
+ # Note: Function Apps are a type of Web App, so we list all web apps
29
+ # and then filter them in the transform stage.
30
+ return [app.as_dict() for app in client.web_apps.list()]
31
+
32
+ except ClientAuthenticationError as e:
33
+ logger.warning(
34
+ (
35
+ "Failed to authenticate to get function apps for subscription '%s'. "
36
+ "Please check your credentials. Error: %s"
37
+ ),
38
+ subscription_id,
39
+ e,
40
+ )
41
+ return []
42
+
43
+ except HttpResponseError as e:
44
+ logger.warning(
45
+ (
46
+ "Failed to get function apps for subscription '%s' due to an API error. "
47
+ "Status code: %s. Message: %s"
48
+ ),
49
+ subscription_id,
50
+ e.status_code,
51
+ str(e),
52
+ )
53
+ return []
54
+
55
+
56
+ @timeit
57
+ def transform_function_apps(function_apps_response: List[Dict]) -> List[Dict]:
58
+ """
59
+ Transform the raw API response to the dictionary structure that the model expects.
60
+ """
61
+ transformed_apps: List[Dict[str, Any]] = []
62
+ for app in function_apps_response:
63
+ # We only want to ingest resources that are explicitly function apps.
64
+ if "functionapp" in app.get("kind", ""):
65
+ transformed_app = {
66
+ "id": app.get("id"),
67
+ "name": app.get("name"),
68
+ "kind": app.get("kind"),
69
+ "location": app.get("location"),
70
+ "state": app.get("state"),
71
+ "default_host_name": app.get("default_host_name"),
72
+ "https_only": app.get("https_only"),
73
+ }
74
+ transformed_apps.append(transformed_app)
75
+ return transformed_apps
76
+
77
+
78
+ @timeit
79
+ def load_function_apps(
80
+ neo4j_session: neo4j.Session,
81
+ data: List[Dict[str, Any]],
82
+ subscription_id: str,
83
+ update_tag: int,
84
+ ) -> None:
85
+ """
86
+ Load the transformed Azure Function App data to Neo4j.
87
+ """
88
+ load(
89
+ neo4j_session,
90
+ AzureFunctionAppSchema(),
91
+ data,
92
+ lastupdated=update_tag,
93
+ AZURE_SUBSCRIPTION_ID=subscription_id,
94
+ )
95
+
96
+
97
+ @timeit
98
+ def cleanup_function_apps(
99
+ neo4j_session: neo4j.Session, common_job_parameters: Dict
100
+ ) -> None:
101
+ """
102
+ Run the cleanup job for Azure Function Apps.
103
+ """
104
+ GraphJob.from_node_schema(AzureFunctionAppSchema(), common_job_parameters).run(
105
+ neo4j_session
106
+ )
107
+
108
+
109
+ @timeit
110
+ def sync(
111
+ neo4j_session: neo4j.Session,
112
+ credentials: Credentials,
113
+ subscription_id: str,
114
+ update_tag: int,
115
+ common_job_parameters: Dict,
116
+ ) -> None:
117
+ """
118
+ The main sync function for Azure Function Apps.
119
+ """
120
+ logger.info(f"Syncing Azure Function Apps for subscription {subscription_id}.")
121
+ raw_apps = get_function_apps(credentials, subscription_id)
122
+ transformed_apps = transform_function_apps(raw_apps)
123
+ load_function_apps(neo4j_session, transformed_apps, subscription_id, update_tag)
124
+ cleanup_function_apps(neo4j_session, common_job_parameters)
@@ -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,
@@ -6,9 +6,12 @@ from azure.identity import ClientSecretCredential
6
6
  from msgraph import GraphServiceClient
7
7
 
8
8
  from cartography.config import Config
9
+ from cartography.intel.entra.app_role_assignments import sync_app_role_assignments
9
10
  from cartography.intel.entra.applications import sync_entra_applications
11
+ from cartography.intel.entra.federation.aws_identity_center import sync_entra_federation
10
12
  from cartography.intel.entra.groups import sync_entra_groups
11
13
  from cartography.intel.entra.ou import sync_entra_ous
14
+ from cartography.intel.entra.service_principals import sync_service_principals
12
15
  from cartography.intel.entra.users import get_tenant
13
16
  from cartography.intel.entra.users import load_tenant
14
17
  from cartography.intel.entra.users import sync_entra_users
@@ -125,5 +128,33 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
125
128
  common_job_parameters,
126
129
  )
127
130
 
131
+ # Run service principals sync
132
+ await sync_service_principals(
133
+ neo4j_session,
134
+ config.entra_tenant_id,
135
+ config.entra_client_id,
136
+ config.entra_client_secret,
137
+ config.update_tag,
138
+ common_job_parameters,
139
+ )
140
+
141
+ # Run app role assignments sync
142
+ await sync_app_role_assignments(
143
+ neo4j_session,
144
+ config.entra_tenant_id,
145
+ config.entra_client_id,
146
+ config.entra_client_secret,
147
+ config.update_tag,
148
+ common_job_parameters,
149
+ )
150
+
151
+ # Run federation sync (after all resources are synced)
152
+ await sync_entra_federation(
153
+ neo4j_session,
154
+ config.update_tag,
155
+ config.entra_tenant_id,
156
+ common_job_parameters,
157
+ )
158
+
128
159
  # Execute syncs in sequence
129
160
  asyncio.run(main())