cartography 0.110.0rc1__py3-none-any.whl → 0.110.0rc2__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 (43) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +0 -8
  3. cartography/config.py +0 -9
  4. cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json +2 -2
  5. cartography/intel/aws/cognito.py +201 -0
  6. cartography/intel/aws/ecs.py +7 -1
  7. cartography/intel/aws/glue.py +64 -0
  8. cartography/intel/aws/kms.py +13 -1
  9. cartography/intel/aws/rds.py +105 -0
  10. cartography/intel/aws/resources.py +2 -0
  11. cartography/intel/aws/route53.py +3 -1
  12. cartography/intel/aws/s3.py +104 -0
  13. cartography/intel/entra/__init__.py +41 -43
  14. cartography/intel/entra/applications.py +2 -1
  15. cartography/intel/entra/ou.py +1 -1
  16. cartography/intel/github/__init__.py +21 -25
  17. cartography/intel/github/repos.py +4 -36
  18. cartography/intel/kubernetes/__init__.py +4 -0
  19. cartography/intel/kubernetes/rbac.py +464 -0
  20. cartography/intel/kubernetes/util.py +17 -0
  21. cartography/models/aws/cognito/__init__.py +0 -0
  22. cartography/models/aws/cognito/identity_pool.py +70 -0
  23. cartography/models/aws/cognito/user_pool.py +47 -0
  24. cartography/models/aws/ec2/security_groups.py +1 -1
  25. cartography/models/aws/ecs/services.py +17 -0
  26. cartography/models/aws/ecs/tasks.py +1 -0
  27. cartography/models/aws/glue/job.py +69 -0
  28. cartography/models/aws/rds/event_subscription.py +146 -0
  29. cartography/models/aws/route53/dnsrecord.py +21 -0
  30. cartography/models/github/dependencies.py +1 -2
  31. cartography/models/kubernetes/clusterrolebindings.py +98 -0
  32. cartography/models/kubernetes/clusterroles.py +52 -0
  33. cartography/models/kubernetes/rolebindings.py +119 -0
  34. cartography/models/kubernetes/roles.py +76 -0
  35. cartography/models/kubernetes/serviceaccounts.py +77 -0
  36. {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/METADATA +3 -3
  37. {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/RECORD +42 -31
  38. cartography/intel/entra/resources.py +0 -20
  39. /cartography/data/jobs/{analysis → scoped_analysis}/aws_s3acl_analysis.json +0 -0
  40. {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/WHEEL +0 -0
  41. {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/entry_points.txt +0 -0
  42. {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/licenses/LICENSE +0 -0
  43. {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/top_level.txt +0 -0
cartography/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.110.0rc1'
21
- __version_tuple__ = version_tuple = (0, 110, 0, 'rc1')
20
+ __version__ = version = '0.110.0rc2'
21
+ __version_tuple__ = version_tuple = (0, 110, 0, 'rc2')
cartography/cli.py CHANGED
@@ -254,14 +254,6 @@ class CLI:
254
254
  "The name of environment variable containing Entra Client Secret for Service Principal Authentication."
255
255
  ),
256
256
  )
257
- parser.add_argument(
258
- "--entra-best-effort-mode",
259
- action="store_true",
260
- help=(
261
- "Enable Entra ID sync best effort mode. This will allow cartography to continue "
262
- "syncing other Entra ID entities and delay raising an exception until the end of the sync."
263
- ),
264
- )
265
257
  parser.add_argument(
266
258
  "--aws-requested-syncs",
267
259
  type=str,
cartography/config.py CHANGED
@@ -51,9 +51,6 @@ class Config:
51
51
  :param entra_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
52
52
  :type entra_client_secret: str
53
53
  :param entra_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
54
- :type entra_best_effort_mode: bool
55
- :param entra_best_effort_mode: If True, Entra ID sync will continue on errors and raise an aggregated
56
- exception at the end of the sync. If False (default), exceptions will be raised immediately.
57
54
  :type aws_requested_syncs: str
58
55
  :param aws_requested_syncs: Comma-separated list of AWS resources to sync. Optional.
59
56
  :type aws_guardduty_severity_threshold: str
@@ -167,8 +164,6 @@ class Config:
167
164
  :param sentinelone_api_url: SentinelOne API URL. Optional.
168
165
  :type sentinelone_api_token: string
169
166
  :param sentinelone_api_token: SentinelOne API token for authentication. Optional.
170
- :type sentinelone_api_token_env_var: string
171
- :param sentinelone_api_token_env_var: The name of an environment variable containing the SentinelOne API token. Optional.
172
167
  :type sentinelone_account_ids: list[str]
173
168
  :param sentinelone_account_ids: List of SentinelOne account IDs to sync. Optional.
174
169
  """
@@ -194,7 +189,6 @@ class Config:
194
189
  entra_tenant_id=None,
195
190
  entra_client_id=None,
196
191
  entra_client_secret=None,
197
- entra_best_effort_mode=False,
198
192
  aws_requested_syncs=None,
199
193
  aws_guardduty_severity_threshold=None,
200
194
  analysis_job_directory=None,
@@ -257,7 +251,6 @@ class Config:
257
251
  scaleway_org=None,
258
252
  sentinelone_api_url=None,
259
253
  sentinelone_api_token=None,
260
- sentinelone_api_token_env_var=None,
261
254
  sentinelone_account_ids=None,
262
255
  ):
263
256
  self.neo4j_uri = neo4j_uri
@@ -281,7 +274,6 @@ class Config:
281
274
  self.entra_tenant_id = entra_tenant_id
282
275
  self.entra_client_id = entra_client_id
283
276
  self.entra_client_secret = entra_client_secret
284
- self.entra_best_effort_mode = entra_best_effort_mode
285
277
  self.aws_requested_syncs = aws_requested_syncs
286
278
  self.aws_guardduty_severity_threshold = aws_guardduty_severity_threshold
287
279
  self.analysis_job_directory = analysis_job_directory
@@ -344,5 +336,4 @@ class Config:
344
336
  self.scaleway_org = scaleway_org
345
337
  self.sentinelone_api_url = sentinelone_api_url
346
338
  self.sentinelone_api_token = sentinelone_api_token
347
- self.sentinelone_api_token_env_var = sentinelone_api_token_env_var
348
339
  self.sentinelone_account_ids = sentinelone_account_ids
@@ -22,8 +22,8 @@
22
22
  "iterative": false
23
23
  },
24
24
  {
25
- "__comment__": "Attach EC2KeyPairs with matching fingerprints to eachother and set duplicate_keyfingerprint = True",
26
- "query": "MATCH (k1:EC2KeyPair), (k2:EC2KeyPair) WHERE k1.id <> k2.id AND k1.keyfingerprint = k2.keyfingerprint SET k1.duplicate_keyfingerprint = True, k2.duplicate_keyfingerprint = True MERGE (k1)-[r:MATCHING_FINGERPRINT]-(k2) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG return COUNT(*) as TotalCompleted",
25
+ "__comment__": "Attach EC2KeyPairs with matching fingerprints to each other and set duplicate_keyfingerprint = True. Use id(k1) < id(k2) to avoid Cartesian product warning and ensure O(1) comparison.",
26
+ "query": "MATCH (k1:EC2KeyPair) MATCH (k2:EC2KeyPair) WHERE id(k1) < id(k2) AND k1.keyfingerprint = k2.keyfingerprint SET k1.duplicate_keyfingerprint = True, k2.duplicate_keyfingerprint = True MERGE (k1)-[r:MATCHING_FINGERPRINT]-(k2) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG RETURN COUNT(*) as TotalCompleted",
27
27
  "iterative": false
28
28
  }
29
29
  ]
@@ -0,0 +1,201 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import boto3
7
+ import neo4j
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.aws.ec2.util import get_botocore_config
12
+ from cartography.models.aws.cognito.identity_pool import CognitoIdentityPoolSchema
13
+ from cartography.models.aws.cognito.user_pool import CognitoUserPoolSchema
14
+ from cartography.util import aws_handle_regions
15
+ from cartography.util import timeit
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @timeit
21
+ @aws_handle_regions
22
+ def get_identity_pools(
23
+ boto3_session: boto3.Session, region: str
24
+ ) -> List[Dict[str, Any]]:
25
+ client = boto3_session.client(
26
+ "cognito-identity", region_name=region, config=get_botocore_config()
27
+ )
28
+ paginator = client.get_paginator("list_identity_pools")
29
+
30
+ all_identity_pools = []
31
+
32
+ for page in paginator.paginate(MaxResults=50):
33
+ identity_pools = page.get("IdentityPools", [])
34
+ all_identity_pools.extend(identity_pools)
35
+ return all_identity_pools
36
+
37
+
38
+ @timeit
39
+ @aws_handle_regions
40
+ def get_identity_pool_roles(
41
+ boto3_session: boto3.Session, identity_pools: List[Dict[str, Any]], region: str
42
+ ) -> List[Dict[str, Any]]:
43
+ client = boto3_session.client(
44
+ "cognito-identity", region_name=region, config=get_botocore_config()
45
+ )
46
+ all_identity_pool_details = []
47
+ for identity_pool in identity_pools:
48
+ response = client.get_identity_pool_roles(
49
+ IdentityPoolId=identity_pool["IdentityPoolId"]
50
+ )
51
+ all_identity_pool_details.append(response)
52
+ return all_identity_pool_details
53
+
54
+
55
+ @timeit
56
+ @aws_handle_regions
57
+ def get_user_pools(boto3_session: boto3.Session, region: str) -> List[Dict[str, Any]]:
58
+ client = boto3_session.client(
59
+ "cognito-idp", region_name=region, config=get_botocore_config()
60
+ )
61
+ paginator = client.get_paginator("list_user_pools")
62
+ all_user_pools = []
63
+
64
+ for page in paginator.paginate(MaxResults=50):
65
+ user_pools = page.get("UserPools", [])
66
+ all_user_pools.extend(user_pools)
67
+ return all_user_pools
68
+
69
+
70
+ def transform_identity_pools(
71
+ identity_pools: List[Dict[str, Any]], region: str
72
+ ) -> List[Dict[str, Any]]:
73
+ transformed_identity_pools = []
74
+ for pool in identity_pools:
75
+ transformed_pool = {
76
+ "IdentityPoolId": pool["IdentityPoolId"],
77
+ "Region": region,
78
+ "Roles": list(pool.get("Roles", {}).values()),
79
+ }
80
+ transformed_identity_pools.append(transformed_pool)
81
+ return transformed_identity_pools
82
+
83
+
84
+ def transform_user_pools(
85
+ user_pools: List[Dict[str, Any]], region: str
86
+ ) -> List[Dict[str, Any]]:
87
+ transformed_user_pools = []
88
+ for pool in user_pools:
89
+ transformed_pool = {
90
+ "Id": pool["Id"],
91
+ "Region": region,
92
+ "Name": pool["Name"],
93
+ "Status": pool.get("Status"),
94
+ }
95
+ transformed_user_pools.append(transformed_pool)
96
+ return transformed_user_pools
97
+
98
+
99
+ @timeit
100
+ def load_identity_pools(
101
+ neo4j_session: neo4j.Session,
102
+ data: List[Dict[str, Any]],
103
+ region: str,
104
+ current_aws_account_id: str,
105
+ aws_update_tag: int,
106
+ ) -> None:
107
+ logger.info(
108
+ f"Loading Cognito Identity Pools {len(data)} for region '{region}' into graph.",
109
+ )
110
+ load(
111
+ neo4j_session,
112
+ CognitoIdentityPoolSchema(),
113
+ data,
114
+ lastupdated=aws_update_tag,
115
+ Region=region,
116
+ AWS_ID=current_aws_account_id,
117
+ )
118
+
119
+
120
+ @timeit
121
+ def load_user_pools(
122
+ neo4j_session: neo4j.Session,
123
+ data: List[Dict[str, Any]],
124
+ region: str,
125
+ current_aws_account_id: str,
126
+ aws_update_tag: int,
127
+ ) -> None:
128
+ logger.info(
129
+ f"Loading Cognito User Pools {len(data)} for region '{region}' into graph.",
130
+ )
131
+ load(
132
+ neo4j_session,
133
+ CognitoUserPoolSchema(),
134
+ data,
135
+ lastupdated=aws_update_tag,
136
+ Region=region,
137
+ AWS_ID=current_aws_account_id,
138
+ )
139
+
140
+
141
+ @timeit
142
+ def cleanup(
143
+ neo4j_session: neo4j.Session,
144
+ common_job_parameters: Dict[str, Any],
145
+ ) -> None:
146
+ logger.debug("Running Efs cleanup job.")
147
+ GraphJob.from_node_schema(CognitoIdentityPoolSchema(), common_job_parameters).run(
148
+ neo4j_session
149
+ )
150
+ GraphJob.from_node_schema(CognitoUserPoolSchema(), common_job_parameters).run(
151
+ neo4j_session
152
+ )
153
+
154
+
155
+ @timeit
156
+ def sync(
157
+ neo4j_session: neo4j.Session,
158
+ boto3_session: boto3.session.Session,
159
+ regions: List[str],
160
+ current_aws_account_id: str,
161
+ update_tag: int,
162
+ common_job_parameters: Dict[str, Any],
163
+ ) -> None:
164
+ for region in regions:
165
+ logger.info(
166
+ f"Syncing Cognito Identity Pools for region '{region}' in account '{current_aws_account_id}'.",
167
+ )
168
+
169
+ identity_pools = get_identity_pools(boto3_session, region)
170
+ if not identity_pools:
171
+ logger.info(
172
+ f"No Cognito Identity Pools found in region '{region}'. Skipping sync."
173
+ )
174
+ else:
175
+ identity_pool_roles = get_identity_pool_roles(
176
+ boto3_session, identity_pools, region
177
+ )
178
+ transformed_identity_pools = transform_identity_pools(
179
+ identity_pool_roles, region
180
+ )
181
+
182
+ load_identity_pools(
183
+ neo4j_session,
184
+ transformed_identity_pools,
185
+ region,
186
+ current_aws_account_id,
187
+ update_tag,
188
+ )
189
+
190
+ user_pools = get_user_pools(boto3_session, region)
191
+ transformed_user_pools = transform_user_pools(user_pools, region)
192
+
193
+ load_user_pools(
194
+ neo4j_session,
195
+ transformed_user_pools,
196
+ region,
197
+ current_aws_account_id,
198
+ update_tag,
199
+ )
200
+
201
+ cleanup(neo4j_session, common_job_parameters)
@@ -171,9 +171,15 @@ def _get_containers_from_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, An
171
171
 
172
172
  def transform_ecs_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, Any]]:
173
173
  """
174
- Extract network interface ID from task attachments.
174
+ Extract network interface ID from task attachments and service name from group.
175
175
  """
176
176
  for task in tasks:
177
+ # Extract serviceName from group field
178
+ group = task.get("group")
179
+ if group and group.startswith("service:"):
180
+ task["serviceName"] = group.split("service:", 1)[1]
181
+
182
+ # Extract network interface ID from task attachments
177
183
  for attachment in task.get("attachments", []):
178
184
  if attachment.get("type") == "ElasticNetworkInterface":
179
185
  details = attachment.get("details", [])
@@ -10,6 +10,7 @@ from cartography.client.core.tx import load
10
10
  from cartography.graph.job import GraphJob
11
11
  from cartography.intel.aws.ec2.util import get_botocore_config
12
12
  from cartography.models.aws.glue.connection import GlueConnectionSchema
13
+ from cartography.models.aws.glue.job import GlueJobSchema
13
14
  from cartography.util import aws_handle_regions
14
15
  from cartography.util import timeit
15
16
 
@@ -32,6 +33,37 @@ def get_glue_connections(
32
33
  return connections
33
34
 
34
35
 
36
+ @timeit
37
+ @aws_handle_regions
38
+ def get_glue_jobs(boto3_session: boto3.Session, region: str) -> List[Dict[str, Any]]:
39
+ client = boto3_session.client(
40
+ "glue", region_name=region, config=get_botocore_config()
41
+ )
42
+ paginator = client.get_paginator("get_jobs")
43
+ jobs = []
44
+ for page in paginator.paginate():
45
+ jobs.extend(page.get("Jobs", []))
46
+ return jobs
47
+
48
+
49
+ def transform_glue_job(jobs: List[Dict[str, Any]], region: str) -> List[Dict[str, Any]]:
50
+ """
51
+ Transform Glue job data for ingestion
52
+ """
53
+ transformed_jobs = []
54
+ for job in jobs:
55
+ transformed_job = {
56
+ "Name": job["Name"],
57
+ "ProfileName": job.get("ProfileName"),
58
+ "JobMode": job.get("JobMode"),
59
+ "Connections": job.get("Connections", {}).get("Connections"),
60
+ "Region": region,
61
+ "Description": job.get("Description"),
62
+ }
63
+ transformed_jobs.append(transformed_job)
64
+ return transformed_jobs
65
+
66
+
35
67
  def transform_glue_connections(
36
68
  connections: List[Dict[str, Any]], region: str
37
69
  ) -> List[Dict[str, Any]]:
@@ -79,6 +111,27 @@ def load_glue_connections(
79
111
  )
80
112
 
81
113
 
114
+ @timeit
115
+ def load_glue_jobs(
116
+ neo4j_session: neo4j.Session,
117
+ data: List[Dict[str, Any]],
118
+ region: str,
119
+ current_aws_account_id: str,
120
+ aws_update_tag: int,
121
+ ) -> None:
122
+ logger.info(
123
+ f"Loading Glue {len(data)} jobs for region '{region}' into graph.",
124
+ )
125
+ load(
126
+ neo4j_session,
127
+ GlueJobSchema(),
128
+ data,
129
+ lastupdated=aws_update_tag,
130
+ Region=region,
131
+ AWS_ID=current_aws_account_id,
132
+ )
133
+
134
+
82
135
  @timeit
83
136
  def cleanup(
84
137
  neo4j_session: neo4j.Session,
@@ -88,6 +141,7 @@ def cleanup(
88
141
  GraphJob.from_node_schema(GlueConnectionSchema(), common_job_parameters).run(
89
142
  neo4j_session
90
143
  )
144
+ GraphJob.from_node_schema(GlueJobSchema(), common_job_parameters).run(neo4j_session)
91
145
 
92
146
 
93
147
  @timeit
@@ -114,4 +168,14 @@ def sync(
114
168
  update_tag,
115
169
  )
116
170
 
171
+ jobs = get_glue_jobs(boto3_session, region)
172
+ transformed_jobs = transform_glue_job(jobs, region)
173
+ load_glue_jobs(
174
+ neo4j_session,
175
+ transformed_jobs,
176
+ region,
177
+ current_aws_account_id,
178
+ update_tag,
179
+ )
180
+
117
181
  cleanup(neo4j_session, common_job_parameters)
@@ -76,8 +76,8 @@ def get_policy(key: Dict, client: botocore.client.BaseClient) -> Any:
76
76
  try:
77
77
  policy = client.get_key_policy(KeyId=key["KeyId"], PolicyName="default")
78
78
  except ClientError as e:
79
- policy = None
80
79
  if e.response["Error"]["Code"] == "AccessDeniedException":
80
+ policy = None
81
81
  logger.warning(
82
82
  f"kms:get_key_policy on key id {key['KeyId']} failed with AccessDeniedException; continuing sync.",
83
83
  exc_info=True,
@@ -187,6 +187,18 @@ def transform_kms_key_policies(
187
187
  policy_data = {}
188
188
 
189
189
  for key_id, policy, *_ in policy_alias_grants_data:
190
+ # Handle keys with null policy (access denied)
191
+ if policy is None:
192
+ logger.info(
193
+ f"Skipping KMS key {key_id} policy due to AccessDenied; policy analysis properties will be null"
194
+ )
195
+ policy_data[key_id] = {
196
+ "kms_key": key_id,
197
+ "anonymous_access": None,
198
+ "anonymous_actions": None,
199
+ }
200
+ continue
201
+
190
202
  parsed_policy = parse_policy(key_id, policy)
191
203
  policy_data[key_id] = parsed_policy
192
204
 
@@ -9,6 +9,7 @@ import neo4j
9
9
  from cartography.client.core.tx import load
10
10
  from cartography.graph.job import GraphJob
11
11
  from cartography.models.aws.rds.cluster import RDSClusterSchema
12
+ from cartography.models.aws.rds.event_subscription import RDSEventSubscriptionSchema
12
13
  from cartography.models.aws.rds.instance import RDSInstanceSchema
13
14
  from cartography.models.aws.rds.snapshot import RDSSnapshotSchema
14
15
  from cartography.models.aws.rds.subnet_group import DBSubnetGroupSchema
@@ -136,6 +137,38 @@ def load_rds_snapshots(
136
137
  )
137
138
 
138
139
 
140
+ @timeit
141
+ @aws_handle_regions
142
+ def get_rds_event_subscription_data(
143
+ boto3_session: boto3.session.Session,
144
+ region: str,
145
+ ) -> List[Dict[str, Any]]:
146
+ client = boto3_session.client("rds", region_name=region)
147
+ paginator = client.get_paginator("describe_event_subscriptions")
148
+ subscriptions = []
149
+ for page in paginator.paginate():
150
+ subscriptions.extend(page["EventSubscriptionsList"])
151
+ return subscriptions
152
+
153
+
154
+ @timeit
155
+ def load_rds_event_subscriptions(
156
+ neo4j_session: neo4j.Session,
157
+ data: List[Dict],
158
+ region: str,
159
+ current_aws_account_id: str,
160
+ aws_update_tag: int,
161
+ ) -> None:
162
+ load(
163
+ neo4j_session,
164
+ RDSEventSubscriptionSchema(),
165
+ data,
166
+ lastupdated=aws_update_tag,
167
+ Region=region,
168
+ AWS_ID=current_aws_account_id,
169
+ )
170
+
171
+
139
172
  def _validate_rds_endpoint(rds: Dict) -> Dict:
140
173
  """
141
174
  Get Endpoint from RDS data structure. Log to debug if an Endpoint field does not exist.
@@ -292,6 +325,28 @@ def transform_rds_instances(
292
325
  return instances
293
326
 
294
327
 
328
+ def transform_rds_event_subscriptions(data: List[Dict]) -> List[Dict]:
329
+ subscriptions = []
330
+ for subscription in data:
331
+ transformed = {
332
+ "CustSubscriptionId": subscription.get("CustSubscriptionId"),
333
+ "EventSubscriptionArn": subscription.get("EventSubscriptionArn"),
334
+ "CustomerAwsId": subscription.get("CustomerAwsId"),
335
+ "SnsTopicArn": subscription.get("SnsTopicArn"),
336
+ "SourceType": subscription.get("SourceType"),
337
+ "Status": subscription.get("Status"),
338
+ "Enabled": subscription.get("Enabled"),
339
+ "SubscriptionCreationTime": dict_value_to_str(
340
+ subscription, "SubscriptionCreationTime"
341
+ ),
342
+ "event_categories": subscription.get("EventCategoriesList") or None,
343
+ "source_ids": subscription.get("SourceIdsList") or None,
344
+ "lastupdated": None, # This will be set by the loader
345
+ }
346
+ subscriptions.append(transformed)
347
+ return subscriptions
348
+
349
+
295
350
  def transform_rds_subnet_groups(
296
351
  data: List[Dict], region: str, current_aws_account_id: str
297
352
  ) -> List[Dict]:
@@ -412,6 +467,20 @@ def cleanup_rds_snapshots(
412
467
  )
413
468
 
414
469
 
470
+ @timeit
471
+ def cleanup_rds_event_subscriptions(
472
+ neo4j_session: neo4j.Session,
473
+ common_job_parameters: Dict,
474
+ ) -> None:
475
+ """
476
+ Remove RDS event subscriptions that weren't updated in this sync run
477
+ """
478
+ logger.debug("Running RDS event subscriptions cleanup job")
479
+ GraphJob.from_node_schema(RDSEventSubscriptionSchema(), common_job_parameters).run(
480
+ neo4j_session
481
+ )
482
+
483
+
415
484
  @timeit
416
485
  def sync_rds_clusters(
417
486
  neo4j_session: neo4j.Session,
@@ -498,6 +567,32 @@ def sync_rds_snapshots(
498
567
  cleanup_rds_snapshots(neo4j_session, common_job_parameters)
499
568
 
500
569
 
570
+ @timeit
571
+ def sync_rds_event_subscriptions(
572
+ neo4j_session: neo4j.Session,
573
+ boto3_session: boto3.session.Session,
574
+ regions: List[str],
575
+ current_aws_account_id: str,
576
+ update_tag: int,
577
+ common_job_parameters: Dict,
578
+ ) -> None:
579
+ """
580
+ Grab RDS event subscription data from AWS, ingest to neo4j, and run the cleanup job.
581
+ """
582
+ for region in regions:
583
+ logger.info(
584
+ "Syncing RDS event subscriptions for region '%s' in account '%s'.",
585
+ region,
586
+ current_aws_account_id,
587
+ )
588
+ data = get_rds_event_subscription_data(boto3_session, region)
589
+ transformed = transform_rds_event_subscriptions(data)
590
+ load_rds_event_subscriptions(
591
+ neo4j_session, transformed, region, current_aws_account_id, update_tag
592
+ )
593
+ cleanup_rds_event_subscriptions(neo4j_session, common_job_parameters)
594
+
595
+
501
596
  @timeit
502
597
  def sync(
503
598
  neo4j_session: neo4j.Session,
@@ -531,6 +626,16 @@ def sync(
531
626
  update_tag,
532
627
  common_job_parameters,
533
628
  )
629
+
630
+ sync_rds_event_subscriptions(
631
+ neo4j_session,
632
+ boto3_session,
633
+ regions,
634
+ current_aws_account_id,
635
+ update_tag,
636
+ common_job_parameters,
637
+ )
638
+
534
639
  merge_module_sync_metadata(
535
640
  neo4j_session,
536
641
  group_type="AWSAccount",
@@ -9,6 +9,7 @@ from . import cloudtrail
9
9
  from . import cloudtrail_management_events
10
10
  from . import cloudwatch
11
11
  from . import codebuild
12
+ from . import cognito
12
13
  from . import config
13
14
  from . import dynamodb
14
15
  from . import ecr
@@ -116,6 +117,7 @@ RESOURCE_FUNCTIONS: Dict[str, Callable[..., None]] = {
116
117
  "efs": efs.sync,
117
118
  "guardduty": guardduty.sync,
118
119
  "codebuild": codebuild.sync,
120
+ "cognito": cognito.sync,
119
121
  "eventbridge": eventbridge.sync,
120
122
  "glue": glue.sync,
121
123
  }
@@ -398,7 +398,9 @@ def link_sub_zones(
398
398
  MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(z:AWSDNSZone)
399
399
  <-[:MEMBER_OF_DNS_ZONE]-(record:DNSRecord{type:"NS"})
400
400
  -[:DNS_POINTS_TO]->(ns:NameServer)<-[:NAMESERVER]-(z2:AWSDNSZone)
401
- WHERE record.name=z2.name AND NOT z=z2
401
+ WHERE record.name = z2.name AND
402
+ z2.name ENDS WITH '.' + z.name AND
403
+ NOT z = z2
402
404
  RETURN z.id as zone_id, z2.id as subzone_id
403
405
  """
404
406
  zone_to_subzone = neo4j_session.read_transaction(