cartography 0.108.0rc2__py3-none-any.whl → 0.109.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 (70) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +14 -0
  3. cartography/config.py +4 -0
  4. cartography/data/indexes.cypher +0 -17
  5. cartography/data/jobs/cleanup/gcp_compute_vpc_cleanup.json +0 -12
  6. cartography/intel/aws/cloudtrail_management_events.py +57 -3
  7. cartography/intel/aws/ecr.py +55 -80
  8. cartography/intel/aws/eventbridge.py +91 -0
  9. cartography/intel/aws/glue.py +117 -0
  10. cartography/intel/aws/identitycenter.py +71 -23
  11. cartography/intel/aws/kms.py +160 -200
  12. cartography/intel/aws/lambda_function.py +206 -190
  13. cartography/intel/aws/rds.py +243 -458
  14. cartography/intel/aws/resourcegroupstaggingapi.py +77 -18
  15. cartography/intel/aws/resources.py +4 -0
  16. cartography/intel/aws/route53.py +334 -332
  17. cartography/intel/aws/secretsmanager.py +62 -44
  18. cartography/intel/entra/groups.py +29 -1
  19. cartography/intel/gcp/__init__.py +10 -0
  20. cartography/intel/gcp/compute.py +19 -42
  21. cartography/intel/trivy/__init__.py +73 -13
  22. cartography/intel/trivy/scanner.py +115 -92
  23. cartography/models/aws/ecr/__init__.py +0 -0
  24. cartography/models/aws/ecr/image.py +41 -0
  25. cartography/models/aws/ecr/repository.py +72 -0
  26. cartography/models/aws/ecr/repository_image.py +95 -0
  27. cartography/models/aws/eventbridge/__init__.py +0 -0
  28. cartography/models/aws/eventbridge/rule.py +77 -0
  29. cartography/models/aws/glue/__init__.py +0 -0
  30. cartography/models/aws/glue/connection.py +51 -0
  31. cartography/models/aws/identitycenter/awspermissionset.py +44 -0
  32. cartography/models/aws/kms/__init__.py +0 -0
  33. cartography/models/aws/kms/aliases.py +86 -0
  34. cartography/models/aws/kms/grants.py +65 -0
  35. cartography/models/aws/kms/keys.py +88 -0
  36. cartography/models/aws/lambda_function/__init__.py +0 -0
  37. cartography/models/aws/lambda_function/alias.py +74 -0
  38. cartography/models/aws/lambda_function/event_source_mapping.py +88 -0
  39. cartography/models/aws/lambda_function/lambda_function.py +89 -0
  40. cartography/models/aws/lambda_function/layer.py +72 -0
  41. cartography/models/aws/rds/__init__.py +0 -0
  42. cartography/models/aws/rds/cluster.py +89 -0
  43. cartography/models/aws/rds/instance.py +154 -0
  44. cartography/models/aws/rds/snapshot.py +108 -0
  45. cartography/models/aws/rds/subnet_group.py +101 -0
  46. cartography/models/aws/route53/__init__.py +0 -0
  47. cartography/models/aws/route53/dnsrecord.py +214 -0
  48. cartography/models/aws/route53/nameserver.py +63 -0
  49. cartography/models/aws/route53/subzone.py +40 -0
  50. cartography/models/aws/route53/zone.py +47 -0
  51. cartography/models/aws/secretsmanager/secret.py +106 -0
  52. cartography/models/entra/group.py +26 -0
  53. cartography/models/entra/user.py +6 -0
  54. cartography/models/gcp/compute/__init__.py +0 -0
  55. cartography/models/gcp/compute/vpc.py +50 -0
  56. cartography/util.py +8 -1
  57. {cartography-0.108.0rc2.dist-info → cartography-0.109.0.dist-info}/METADATA +2 -2
  58. {cartography-0.108.0rc2.dist-info → cartography-0.109.0.dist-info}/RECORD +62 -38
  59. cartography/data/jobs/cleanup/aws_dns_cleanup.json +0 -65
  60. cartography/data/jobs/cleanup/aws_import_identity_center_cleanup.json +0 -16
  61. cartography/data/jobs/cleanup/aws_import_lambda_cleanup.json +0 -50
  62. cartography/data/jobs/cleanup/aws_import_rds_clusters_cleanup.json +0 -23
  63. cartography/data/jobs/cleanup/aws_import_rds_instances_cleanup.json +0 -47
  64. cartography/data/jobs/cleanup/aws_import_rds_snapshots_cleanup.json +0 -23
  65. cartography/data/jobs/cleanup/aws_import_secrets_cleanup.json +0 -8
  66. cartography/data/jobs/cleanup/aws_kms_details.json +0 -10
  67. {cartography-0.108.0rc2.dist-info → cartography-0.109.0.dist-info}/WHEEL +0 -0
  68. {cartography-0.108.0rc2.dist-info → cartography-0.109.0.dist-info}/entry_points.txt +0 -0
  69. {cartography-0.108.0rc2.dist-info → cartography-0.109.0.dist-info}/licenses/LICENSE +0 -0
  70. {cartography-0.108.0rc2.dist-info → cartography-0.109.0.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ import boto3
7
7
  import neo4j
8
8
 
9
9
  from cartography.client.core.tx import load
10
+ from cartography.client.core.tx import load_matchlinks
10
11
  from cartography.graph.job import GraphJob
11
12
  from cartography.models.aws.identitycenter.awsidentitycenter import (
12
13
  AWSIdentityCenterInstanceSchema,
@@ -14,9 +15,11 @@ from cartography.models.aws.identitycenter.awsidentitycenter import (
14
15
  from cartography.models.aws.identitycenter.awspermissionset import (
15
16
  AWSPermissionSetSchema,
16
17
  )
18
+ from cartography.models.aws.identitycenter.awspermissionset import (
19
+ RoleAssignmentAllowedByMatchLink,
20
+ )
17
21
  from cartography.models.aws.identitycenter.awsssouser import AWSSSOUserSchema
18
22
  from cartography.util import aws_handle_regions
19
- from cartography.util import run_cleanup_job
20
23
  from cartography.util import timeit
21
24
 
22
25
  logger = logging.getLogger(__name__)
@@ -120,6 +123,8 @@ def load_permission_sets(
120
123
  InstanceArn=instance_arn,
121
124
  Region=region,
122
125
  AWS_ID=aws_account_id,
126
+ _sub_resource_label="AWSAccount",
127
+ _sub_resource_id=aws_account_id,
123
128
  )
124
129
 
125
130
 
@@ -220,31 +225,64 @@ def get_role_assignments(
220
225
  return role_assignments
221
226
 
222
227
 
228
+ @timeit
229
+ def get_permset_roles(
230
+ neo4j_session: neo4j.Session,
231
+ role_assignments: List[Dict[str, Any]],
232
+ ) -> List[Dict[str, Any]]:
233
+ """
234
+ Enrich role assignments with exact role ARNs by querying existing permission set relationships.
235
+ Uses the ASSIGNED_TO_ROLE relationships created when permission sets were loaded.
236
+ """
237
+ # Get unique permission set ARNs from role assignments
238
+ permset_ids = list({ra["PermissionSetArn"] for ra in role_assignments})
239
+
240
+ query = """
241
+ MATCH (role:AWSRole)<-[:ASSIGNED_TO_ROLE]-(permset:AWSPermissionSet)
242
+ WHERE permset.arn IN $PermSetIds
243
+ RETURN permset.arn AS PermissionSetArn, role.arn AS RoleArn
244
+ """
245
+ result = neo4j_session.run(query, PermSetIds=permset_ids)
246
+ permset_to_role = [record.data() for record in result]
247
+
248
+ # Create mapping from permission set ARN to role ARN
249
+ permset_to_role_map = {
250
+ entry["PermissionSetArn"]: entry["RoleArn"] for entry in permset_to_role
251
+ }
252
+
253
+ # Enrich role assignments with exact role ARNs
254
+ enriched_assignments = []
255
+ for assignment in role_assignments:
256
+ role_arn = permset_to_role_map.get(assignment["PermissionSetArn"])
257
+ enriched_assignments.append(
258
+ {
259
+ **assignment,
260
+ "RoleArn": role_arn,
261
+ }
262
+ )
263
+
264
+ return enriched_assignments
265
+
266
+
223
267
  @timeit
224
268
  def load_role_assignments(
225
269
  neo4j_session: neo4j.Session,
226
270
  role_assignments: List[Dict],
271
+ aws_account_id: str,
227
272
  aws_update_tag: int,
228
273
  ) -> None:
229
274
  """
230
- Load role assignments into the graph
275
+ Load role assignments into the graph using MatchLink schema
231
276
  """
232
277
  logger.info(f"Loading {len(role_assignments)} role assignments")
233
- if role_assignments:
234
- neo4j_session.run(
235
- """
236
- UNWIND $role_assignments AS ra
237
- MATCH (acc:AWSAccount{id:ra.AccountId}) -[:RESOURCE]->
238
- (role:AWSRole)<-[:ASSIGNED_TO_ROLE]-
239
- (permset:AWSPermissionSet {id: ra.PermissionSetArn})
240
- MATCH (sso:AWSSSOUser {id: ra.UserId})
241
- MERGE (role)-[r:ALLOWED_BY]->(sso)
242
- SET r.lastupdated = $aws_update_tag,
243
- r.permission_set_arn = ra.PermissionSetArn
244
- """,
245
- role_assignments=role_assignments,
246
- aws_update_tag=aws_update_tag,
247
- )
278
+ load_matchlinks(
279
+ neo4j_session,
280
+ RoleAssignmentAllowedByMatchLink(),
281
+ role_assignments,
282
+ lastupdated=aws_update_tag,
283
+ _sub_resource_label="AWSAccount",
284
+ _sub_resource_id=aws_account_id,
285
+ )
248
286
 
249
287
 
250
288
  @timeit
@@ -262,11 +300,14 @@ def cleanup(
262
300
  GraphJob.from_node_schema(AWSSSOUserSchema(), common_job_parameters).run(
263
301
  neo4j_session,
264
302
  )
265
- run_cleanup_job(
266
- "aws_import_identity_center_cleanup.json",
267
- neo4j_session,
268
- common_job_parameters,
269
- )
303
+
304
+ # Clean up role assignment MatchLinks
305
+ GraphJob.from_matchlink(
306
+ RoleAssignmentAllowedByMatchLink(),
307
+ "AWSAccount",
308
+ common_job_parameters["AWS_ID"],
309
+ common_job_parameters["UPDATE_TAG"],
310
+ ).run(neo4j_session)
270
311
 
271
312
 
272
313
  @timeit
@@ -327,9 +368,16 @@ def sync_identity_center_instances(
327
368
  instance_arn,
328
369
  region,
329
370
  )
330
- load_role_assignments(
371
+
372
+ # Enrich role assignments with exact role ARNs using permission set relationships
373
+ enriched_role_assignments = get_permset_roles(
331
374
  neo4j_session,
332
375
  role_assignments,
376
+ )
377
+ load_role_assignments(
378
+ neo4j_session,
379
+ enriched_role_assignments,
380
+ current_aws_account_id,
333
381
  update_tag,
334
382
  )
335
383
 
@@ -4,8 +4,6 @@ from typing import Any
4
4
  from typing import Dict
5
5
  from typing import Generator
6
6
  from typing import List
7
- from typing import Optional
8
- from typing import Tuple
9
7
 
10
8
  import boto3
11
9
  import botocore
@@ -13,8 +11,13 @@ import neo4j
13
11
  from botocore.exceptions import ClientError
14
12
  from policyuniverse.policy import Policy
15
13
 
14
+ from cartography.client.core.tx import load
15
+ from cartography.graph.job import GraphJob
16
+ from cartography.models.aws.kms.aliases import KMSAliasSchema
17
+ from cartography.models.aws.kms.grants import KMSGrantSchema
18
+ from cartography.models.aws.kms.keys import KMSKeySchema
16
19
  from cartography.util import aws_handle_regions
17
- from cartography.util import run_cleanup_job
20
+ from cartography.util import dict_date_to_epoch
18
21
  from cartography.util import timeit
19
22
 
20
23
  logger = logging.getLogger(__name__)
@@ -51,7 +54,7 @@ def get_kms_key_list(boto3_session: boto3.session.Session, region: str) -> List[
51
54
  @aws_handle_regions
52
55
  def get_kms_key_details(
53
56
  boto3_session: boto3.session.Session,
54
- kms_key_data: Dict,
57
+ kms_key_data: List[Dict],
55
58
  region: str,
56
59
  ) -> Generator[Any, Any, Any]:
57
60
  """
@@ -120,249 +123,176 @@ def get_grants(key: Dict, client: botocore.client.BaseClient) -> List[Any]:
120
123
 
121
124
 
122
125
  @timeit
123
- def _load_kms_key_aliases(
124
- neo4j_session: neo4j.Session,
125
- aliases: List[Dict],
126
- update_tag: int,
127
- ) -> None:
126
+ def transform_kms_aliases(aliases: List[Dict]) -> List[Dict]:
127
+ """
128
+ Transform AWS KMS Aliases to match the data model.
129
+ Converts datetime fields to epoch timestamps for consistency.
128
130
  """
129
- Ingest KMS Aliases into neo4j.
131
+ transformed_data = []
132
+ for alias in aliases:
133
+ transformed = dict(alias)
134
+
135
+ # Convert datetime fields to epoch timestamps
136
+ transformed["CreationDate"] = dict_date_to_epoch(alias, "CreationDate")
137
+ transformed["LastUpdatedDate"] = dict_date_to_epoch(alias, "LastUpdatedDate")
138
+
139
+ transformed_data.append(transformed)
140
+ return transformed_data
141
+
142
+
143
+ def transform_kms_keys(keys: List[Dict], policy_data: Dict[str, Dict]) -> List[Dict]:
130
144
  """
131
- ingest_aliases = """
132
- UNWIND $alias_list AS alias
133
- MERGE (a:KMSAlias{id: alias.AliasArn})
134
- ON CREATE SET a.firstseen = timestamp(), a.targetkeyid = alias.TargetKeyId
135
- SET a.aliasname = alias.AliasName, a.lastupdated = $UpdateTag
136
- WITH a, alias
137
- MATCH (kmskey:KMSKey{id: alias.TargetKeyId})
138
- MERGE (a)-[r:KNOWN_AS]->(kmskey)
139
- ON CREATE SET r.firstseen = timestamp()
140
- SET r.lastupdated = $UpdateTag
145
+ Transform AWS KMS Keys to match the data model.
146
+ Converts datetime fields to epoch timestamps for consistency.
147
+ Includes policy analysis properties.
141
148
  """
149
+ transformed_data = []
150
+ for key in keys:
151
+ transformed = dict(key)
142
152
 
143
- neo4j_session.run(
144
- ingest_aliases,
145
- alias_list=aliases,
146
- UpdateTag=update_tag,
147
- )
153
+ # Convert datetime fields to epoch timestamps
154
+ transformed["CreationDate"] = dict_date_to_epoch(key, "CreationDate")
155
+ transformed["DeletionDate"] = dict_date_to_epoch(key, "DeletionDate")
156
+ transformed["ValidTo"] = dict_date_to_epoch(key, "ValidTo")
148
157
 
158
+ # Add policy analysis
159
+ transformed.update(policy_data[key["KeyId"]])
149
160
 
150
- @timeit
151
- def _load_kms_key_grants(
152
- neo4j_session: neo4j.Session,
153
- grants_list: List[Dict],
154
- update_tag: int,
155
- ) -> None:
161
+ transformed_data.append(transformed)
162
+ return transformed_data
163
+
164
+
165
+ def transform_kms_grants(grants: List[Dict]) -> List[Dict]:
156
166
  """
157
- Ingest KMS Key Grants into neo4j.
167
+ Transform AWS KMS Grants to match the data model.
168
+ Converts datetime fields to epoch timestamps for consistency.
169
+ """
170
+ transformed_data = []
171
+ for grant in grants:
172
+ transformed = dict(grant)
173
+
174
+ # Convert datetime fields to epoch timestamps
175
+ transformed["CreationDate"] = dict_date_to_epoch(grant, "CreationDate")
176
+
177
+ transformed_data.append(transformed)
178
+ return transformed_data
179
+
180
+
181
+ def transform_kms_key_policies(
182
+ policy_alias_grants_data: list[tuple],
183
+ ) -> dict[str, dict[str, Any]]:
158
184
  """
159
- ingest_grants = """
160
- UNWIND $grants AS grant
161
- MERGE (g:KMSGrant{id: grant.GrantId})
162
- ON CREATE SET g.firstseen = timestamp(), g.granteeprincipal = grant.GranteePrincipal,
163
- g.creationdate = grant.CreationDate
164
- SET g.name = grant.GrantName, g.lastupdated = $UpdateTag
165
- WITH g, grant
166
- MATCH (kmskey:KMSKey{id: grant.KeyId})
167
- MERGE (g)-[r:APPLIED_ON]->(kmskey)
168
- ON CREATE SET r.firstseen = timestamp()
169
- SET r.lastupdated = $UpdateTag
185
+ Transform KMS key policy data for inclusion in key records.
170
186
  """
187
+ policy_data = {}
171
188
 
172
- # neo4j does not accept datetime objects and values. This loop is used to convert
173
- # these values to string.
174
- for grant in grants_list:
175
- grant["CreationDate"] = str(grant["CreationDate"])
189
+ for key_id, policy, *_ in policy_alias_grants_data:
190
+ parsed_policy = parse_policy(key_id, policy)
191
+ policy_data[key_id] = parsed_policy
176
192
 
177
- neo4j_session.run(
178
- ingest_grants,
179
- grants=grants_list,
180
- UpdateTag=update_tag,
181
- )
193
+ return policy_data
182
194
 
183
195
 
184
196
  @timeit
185
- def _load_kms_key_policies(
197
+ def load_kms_aliases(
186
198
  neo4j_session: neo4j.Session,
187
- policies: List[Dict],
199
+ aliases: List[Dict],
200
+ region: str,
201
+ aws_account_id: str,
188
202
  update_tag: int,
189
203
  ) -> None:
190
204
  """
191
- Ingest KMS Key policy results into neo4j.
192
- """
193
- # NOTE we use the coalesce function so appending works when the value is null initially
194
- ingest_policies = """
195
- UNWIND $policies AS policy
196
- MATCH (k:KMSKey) where k.name = policy.kms_key
197
- SET k.anonymous_access = (coalesce(k.anonymous_access, false) OR policy.internet_accessible),
198
- k.anonymous_actions = coalesce(k.anonymous_actions, []) + policy.accessible_actions,
199
- k.lastupdated = $UpdateTag
200
- """
201
-
202
- neo4j_session.run(
203
- ingest_policies,
204
- policies=policies,
205
- UpdateTag=update_tag,
206
- )
207
-
208
-
209
- def _set_default_values(neo4j_session: neo4j.Session, aws_account_id: str) -> None:
210
- set_defaults = """
211
- MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(kmskey:KMSKey) where kmskey.anonymous_actions IS NULL
212
- SET kmskey.anonymous_access = false, kmskey.anonymous_actions = []
205
+ Load KMS Aliases into Neo4j using the data model.
213
206
  """
214
-
215
- neo4j_session.run(
216
- set_defaults,
207
+ logger.info(f"Loading {len(aliases)} KMS aliases for region {region} into graph.")
208
+ load(
209
+ neo4j_session,
210
+ KMSAliasSchema(),
211
+ aliases,
212
+ lastupdated=update_tag,
213
+ Region=region,
217
214
  AWS_ID=aws_account_id,
218
215
  )
219
216
 
220
217
 
221
218
  @timeit
222
- def load_kms_key_details(
219
+ def load_kms_grants(
223
220
  neo4j_session: neo4j.Session,
224
- policy_alias_grants_data: List[Tuple[Any, Any, Any, Any]],
225
- region: str,
221
+ grants: List[Dict],
226
222
  aws_account_id: str,
227
223
  update_tag: int,
228
224
  ) -> None:
229
225
  """
230
- Create dictionaries for all KMS key policies, aliases and grants so we can import them in a single query for each
226
+ Load KMS Grants into Neo4j using the data model.
231
227
  """
232
- policies = []
233
- aliases: List[str] = []
234
- grants: List[str] = []
235
- for key, policy, alias, grant in policy_alias_grants_data:
236
- parsed_policy = parse_policy(key, policy)
237
- if parsed_policy is not None:
238
- policies.append(parsed_policy)
239
- if len(alias) > 0:
240
- aliases.extend(alias)
241
- if len(grants) > 0:
242
- grants.extend(grant)
243
-
244
- # cleanup existing policy properties
245
- run_cleanup_job(
246
- "aws_kms_details.json",
228
+ logger.info(f"Loading {len(grants)} KMS grants into graph.")
229
+ load(
247
230
  neo4j_session,
248
- {"UPDATE_TAG": update_tag, "AWS_ID": aws_account_id},
231
+ KMSGrantSchema(),
232
+ grants,
233
+ lastupdated=update_tag,
234
+ AWS_ID=aws_account_id,
249
235
  )
250
236
 
251
- _load_kms_key_policies(neo4j_session, policies, update_tag)
252
- _load_kms_key_aliases(neo4j_session, aliases, update_tag)
253
- _load_kms_key_grants(neo4j_session, grants, update_tag)
254
- _set_default_values(neo4j_session, aws_account_id)
255
-
256
237
 
257
238
  @timeit
258
- def parse_policy(key: str, policy: Policy) -> Optional[Dict[Any, Any]]:
239
+ def parse_policy(key: str, policy: Policy) -> dict[str, Any]:
259
240
  """
260
- Uses PolicyUniverse to parse KMS key policies and returns the internet accessibility results
241
+ Uses PolicyUniverse to parse KMS key policies and returns the internet accessibility results.
242
+ Expects policy to never be None
261
243
  """
262
- # policy is not required, so may be None
263
- # policy JSON format. Note condition can be any JSON statement so will need to import as-is
264
- # policy is a very complex format, so the policyuniverse library will be used for parsing out important data
265
- # ...metadata...
266
- # "Policy" :
267
- # {
268
- # "Version": "2012-10-17",
269
- # "Id": "key-consolepolicy-5",
270
- # "Statement": [
271
- # {
272
- # "Sid": "Enable IAM User Permissions",
273
- # "Effect": "Allow",
274
- # "Principal": {
275
- # "AWS": "arn:aws:iam::123456789012:root"
276
- # },
277
- # "Action": "kms:*",
278
- # "Resource": "*"
279
- # },
280
- # {
281
- # "Sid": "Allow access for Key Administrators",
282
- # "Effect": "Allow",
283
- # "Principal": {
284
- # "AWS": "arn:aws:iam::123456789012:role/ec2-manager"
285
- # },
286
- # "Action": [
287
- # "kms:Create*",
288
- # "kms:Describe*",
289
- # "kms:Enable*",
290
- # "kms:List*",
291
- # "kms:Put*",
292
- # "kms:Update*",
293
- # "kms:Revoke*",
294
- # "kms:Disable*",
295
- # "kms:Get*",
296
- # "kms:Delete*",
297
- # "kms:ScheduleKeyDeletion",
298
- # "kms:CancelKeyDeletion"
299
- # ],
300
- # "Resource": "*"
301
- # }
302
- # ]
303
- # }
304
- if policy is not None:
305
- # get just the policy element and convert to JSON because boto3 returns this as string
306
- policy = Policy(json.loads(policy["Policy"]))
307
- if policy.is_internet_accessible():
308
- return {
309
- "kms_key": key,
310
- "internet_accessible": True,
311
- "accessible_actions": list(policy.internet_accessible_actions()),
312
- }
313
- else:
314
- return None
315
- else:
316
- return None
244
+ policy = Policy(json.loads(policy["Policy"]))
245
+ inet_actions = policy.internet_accessible_actions()
246
+
247
+ return {
248
+ "kms_key": key,
249
+ "anonymous_access": policy.is_internet_accessible(),
250
+ "anonymous_actions": list(inet_actions) if inet_actions else [],
251
+ }
317
252
 
318
253
 
319
254
  @timeit
320
255
  def load_kms_keys(
321
256
  neo4j_session: neo4j.Session,
322
- data: Dict,
257
+ keys: List[Dict],
323
258
  region: str,
324
- current_aws_account_id: str,
325
- aws_update_tag: int,
259
+ aws_account_id: str,
260
+ update_tag: int,
326
261
  ) -> None:
327
- ingest_keys = """
328
- UNWIND $key_list AS k
329
- MERGE (kmskey:KMSKey{id:k.KeyId})
330
- ON CREATE SET kmskey.firstseen = timestamp(),
331
- kmskey.arn = k.Arn, kmskey.creationdate = k.CreationDate
332
- SET kmskey.deletiondate = k.DeletionDate,
333
- kmskey.validto = k.ValidTo,
334
- kmskey.enabled = k.Enabled,
335
- kmskey.keystate = k.KeyState,
336
- kmskey.customkeystoreid = k.CustomKeyStoreId,
337
- kmskey.cloudhsmclusterid = k.CloudHsmClusterId,
338
- kmskey.lastupdated = $aws_update_tag,
339
- kmskey.region = $Region
340
- WITH kmskey
341
- MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID})
342
- MERGE (aa)-[r:RESOURCE]->(kmskey)
343
- ON CREATE SET r.firstseen = timestamp()
344
- SET r.lastupdated = $aws_update_tag
345
262
  """
346
-
347
- # neo4j does not accept datetime objects and values. This loop is used to convert
348
- # these values to string.
349
- for key in data:
350
- key["CreationDate"] = str(key["CreationDate"])
351
- key["DeletionDate"] = str(key.get("DeletionDate"))
352
- key["ValidTo"] = str(key.get("ValidTo"))
353
-
354
- neo4j_session.run(
355
- ingest_keys,
356
- key_list=data,
263
+ Load KMS Keys into Neo4j using the data model.
264
+ Expects data to already be transformed by transform_kms_keys().
265
+ """
266
+ logger.info(f"Loading {len(keys)} KMS keys for region {region} into graph.")
267
+ load(
268
+ neo4j_session,
269
+ KMSKeySchema(),
270
+ keys,
271
+ lastupdated=update_tag,
357
272
  Region=region,
358
- AWS_ACCOUNT_ID=current_aws_account_id,
359
- aws_update_tag=aws_update_tag,
273
+ AWS_ID=aws_account_id,
360
274
  )
361
275
 
362
276
 
363
277
  @timeit
364
278
  def cleanup_kms(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None:
365
- run_cleanup_job("aws_import_kms_cleanup.json", neo4j_session, common_job_parameters)
279
+ """
280
+ Run KMS cleanup using schema-based GraphJobs for all node types.
281
+ """
282
+ logger.debug("Running KMS cleanup using GraphJob for all node types")
283
+
284
+ # Clean up grants first (they depend on keys)
285
+ GraphJob.from_node_schema(KMSGrantSchema(), common_job_parameters).run(
286
+ neo4j_session
287
+ )
288
+
289
+ # Clean up aliases
290
+ GraphJob.from_node_schema(KMSAliasSchema(), common_job_parameters).run(
291
+ neo4j_session
292
+ )
293
+
294
+ # Clean up keys
295
+ GraphJob.from_node_schema(KMSKeySchema(), common_job_parameters).run(neo4j_session)
366
296
 
367
297
 
368
298
  @timeit
@@ -373,24 +303,54 @@ def sync_kms_keys(
373
303
  current_aws_account_id: str,
374
304
  aws_update_tag: int,
375
305
  ) -> None:
306
+ # Get basic key metadata
376
307
  kms_keys = get_kms_key_list(boto3_session, region)
377
308
 
309
+ # Get detailed data (policies, aliases, grants)
310
+ policy_alias_grants_data = list(
311
+ get_kms_key_details(boto3_session, kms_keys, region)
312
+ )
313
+
314
+ # Transform policy data for inclusion in keys
315
+ policy_data = transform_kms_key_policies(policy_alias_grants_data)
316
+
317
+ # Transform keys WITH policy data included
318
+ transformed_keys = transform_kms_keys(kms_keys, policy_data)
319
+
320
+ # Load complete keys (now includes policy properties via data model)
378
321
  load_kms_keys(
379
322
  neo4j_session,
380
- kms_keys,
323
+ transformed_keys,
381
324
  region,
382
325
  current_aws_account_id,
383
326
  aws_update_tag,
384
327
  )
385
328
 
386
- policy_alias_grants_data = get_kms_key_details(boto3_session, kms_keys, region)
387
- load_kms_key_details(
329
+ # Extract and transform aliases and grants
330
+ aliases: List[Dict] = []
331
+ grants: List[Dict] = []
332
+
333
+ for key, policy, alias, grant in policy_alias_grants_data:
334
+ if len(alias) > 0:
335
+ aliases.extend(alias)
336
+ if len(grant) > 0:
337
+ grants.extend(grant)
338
+
339
+ # Transform aliases and grants following standard pattern
340
+ transformed_aliases = transform_kms_aliases(aliases)
341
+ transformed_grants = transform_kms_grants(grants)
342
+
343
+ # Load aliases and grants directly - standard Cartography pattern
344
+ load_kms_aliases(
388
345
  neo4j_session,
389
- policy_alias_grants_data,
346
+ transformed_aliases,
390
347
  region,
391
348
  current_aws_account_id,
392
349
  aws_update_tag,
393
350
  )
351
+ load_kms_grants(
352
+ neo4j_session, transformed_grants, current_aws_account_id, aws_update_tag
353
+ )
394
354
 
395
355
 
396
356
  @timeit