cartography 0.104.0rc3__py3-none-any.whl → 0.106.0rc1__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 (75) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +26 -1
  3. cartography/client/aws/__init__.py +19 -0
  4. cartography/client/aws/ecr.py +51 -0
  5. cartography/config.py +8 -0
  6. cartography/data/indexes.cypher +0 -37
  7. cartography/data/jobs/cleanup/aws_import_lambda_cleanup.json +1 -1
  8. cartography/graph/cleanupbuilder.py +151 -41
  9. cartography/intel/aws/acm.py +124 -0
  10. cartography/intel/aws/cloudtrail.py +3 -38
  11. cartography/intel/aws/ecr.py +8 -2
  12. cartography/intel/aws/ecs.py +228 -380
  13. cartography/intel/aws/efs.py +99 -11
  14. cartography/intel/aws/iam.py +1 -1
  15. cartography/intel/aws/identitycenter.py +14 -3
  16. cartography/intel/aws/inspector.py +106 -53
  17. cartography/intel/aws/lambda_function.py +1 -1
  18. cartography/intel/aws/rds.py +2 -1
  19. cartography/intel/aws/resources.py +2 -0
  20. cartography/intel/aws/s3.py +195 -4
  21. cartography/intel/aws/sqs.py +36 -90
  22. cartography/intel/entra/__init__.py +22 -0
  23. cartography/intel/entra/applications.py +366 -0
  24. cartography/intel/entra/groups.py +151 -0
  25. cartography/intel/entra/ou.py +21 -5
  26. cartography/intel/kubernetes/__init__.py +30 -14
  27. cartography/intel/kubernetes/clusters.py +86 -0
  28. cartography/intel/kubernetes/namespaces.py +59 -57
  29. cartography/intel/kubernetes/pods.py +140 -77
  30. cartography/intel/kubernetes/secrets.py +95 -45
  31. cartography/intel/kubernetes/services.py +131 -67
  32. cartography/intel/kubernetes/util.py +125 -14
  33. cartography/intel/trivy/__init__.py +161 -0
  34. cartography/intel/trivy/scanner.py +363 -0
  35. cartography/models/aws/acm/__init__.py +0 -0
  36. cartography/models/aws/acm/certificate.py +75 -0
  37. cartography/models/aws/cloudtrail/trail.py +24 -0
  38. cartography/models/aws/ecs/__init__.py +0 -0
  39. cartography/models/aws/ecs/clusters.py +64 -0
  40. cartography/models/aws/ecs/container_definitions.py +93 -0
  41. cartography/models/aws/ecs/container_instances.py +84 -0
  42. cartography/models/aws/ecs/containers.py +80 -0
  43. cartography/models/aws/ecs/services.py +117 -0
  44. cartography/models/aws/ecs/task_definitions.py +97 -0
  45. cartography/models/aws/ecs/tasks.py +110 -0
  46. cartography/models/aws/efs/file_system.py +60 -0
  47. cartography/models/aws/efs/mount_target.py +29 -2
  48. cartography/models/aws/s3/notification.py +24 -0
  49. cartography/models/aws/secretsmanager/secret_version.py +0 -2
  50. cartography/models/aws/sqs/__init__.py +0 -0
  51. cartography/models/aws/sqs/queue.py +89 -0
  52. cartography/models/core/nodes.py +15 -2
  53. cartography/models/entra/app_role_assignment.py +115 -0
  54. cartography/models/entra/application.py +47 -0
  55. cartography/models/entra/group.py +91 -0
  56. cartography/models/kubernetes/__init__.py +0 -0
  57. cartography/models/kubernetes/clusters.py +26 -0
  58. cartography/models/kubernetes/containers.py +108 -0
  59. cartography/models/kubernetes/namespaces.py +51 -0
  60. cartography/models/kubernetes/pods.py +80 -0
  61. cartography/models/kubernetes/secrets.py +79 -0
  62. cartography/models/kubernetes/services.py +108 -0
  63. cartography/models/trivy/__init__.py +0 -0
  64. cartography/models/trivy/findings.py +66 -0
  65. cartography/models/trivy/fix.py +66 -0
  66. cartography/models/trivy/package.py +71 -0
  67. cartography/sync.py +2 -0
  68. cartography/util.py +15 -10
  69. {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/METADATA +3 -2
  70. {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/RECORD +74 -40
  71. cartography/data/jobs/cleanup/kubernetes_import_cleanup.json +0 -70
  72. {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/WHEEL +0 -0
  73. {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/entry_points.txt +0 -0
  74. {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/licenses/LICENSE +0 -0
  75. {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@ from botocore.exceptions import EndpointConnectionError
17
17
  from policyuniverse.policy import Policy
18
18
 
19
19
  from cartography.stats import get_stats_client
20
+ from cartography.util import aws_handle_regions
20
21
  from cartography.util import merge_module_sync_metadata
21
22
  from cartography.util import run_analysis_job
22
23
  from cartography.util import run_cleanup_job
@@ -54,7 +55,7 @@ def get_s3_bucket_list(boto3_session: boto3.session.Session) -> List[Dict]:
54
55
  def get_s3_bucket_details(
55
56
  boto3_session: boto3.session.Session,
56
57
  bucket_data: Dict,
57
- ) -> Generator[Tuple[str, Dict, Dict, Dict, Dict, Dict], None, None]:
58
+ ) -> Generator[Tuple[str, Dict, Dict, Dict, Dict, Dict, Dict], None, None]:
58
59
  """
59
60
  Iterates over all S3 buckets. Yields bucket name (string), S3 bucket policies (JSON), ACLs (JSON),
60
61
  default encryption policy (JSON), Versioning (JSON), and Public Access Block (JSON)
@@ -69,6 +70,7 @@ def get_s3_bucket_details(
69
70
  Dict[str, Any],
70
71
  Dict[str, Any],
71
72
  Dict[str, Any],
73
+ Dict[str, Any],
72
74
  ]
73
75
 
74
76
  async def _get_bucket_detail(bucket: Dict[str, Any]) -> BucketDetail:
@@ -85,14 +87,24 @@ def get_s3_bucket_details(
85
87
  encryption,
86
88
  versioning,
87
89
  public_access_block,
90
+ bucket_ownership_controls,
88
91
  ) = await asyncio.gather(
89
92
  to_asynchronous(get_acl, bucket, client),
90
93
  to_asynchronous(get_policy, bucket, client),
91
94
  to_asynchronous(get_encryption, bucket, client),
92
95
  to_asynchronous(get_versioning, bucket, client),
93
96
  to_asynchronous(get_public_access_block, bucket, client),
97
+ to_asynchronous(get_bucket_ownership_controls, bucket, client),
98
+ )
99
+ return (
100
+ bucket["Name"],
101
+ acl,
102
+ policy,
103
+ encryption,
104
+ versioning,
105
+ public_access_block,
106
+ bucket_ownership_controls,
94
107
  )
95
- return bucket["Name"], acl, policy, encryption, versioning, public_access_block
96
108
 
97
109
  bucket_details = to_synchronous(
98
110
  *[_get_bucket_detail(bucket) for bucket in bucket_data["Buckets"]],
@@ -204,6 +216,31 @@ def get_public_access_block(
204
216
  return public_access_block
205
217
 
206
218
 
219
+ @timeit
220
+ def get_bucket_ownership_controls(
221
+ bucket: Dict, client: botocore.client.BaseClient
222
+ ) -> Optional[Dict]:
223
+ """
224
+ Gets the S3 object ownership controls configuration.
225
+ """
226
+ bucket_ownership_controls = None
227
+ try:
228
+ bucket_ownership_controls = client.get_bucket_ownership_controls(
229
+ Bucket=bucket["Name"]
230
+ )
231
+ except ClientError as e:
232
+ if _is_common_exception(e, bucket):
233
+ pass
234
+ else:
235
+ raise
236
+ except EndpointConnectionError:
237
+ logger.warning(
238
+ f"Failed to retrieve S3 bucket ownership controls for {bucket['Name']}"
239
+ " - Could not connect to the endpoint URL",
240
+ )
241
+ return bucket_ownership_controls
242
+
243
+
207
244
  @timeit
208
245
  def _is_common_exception(e: Exception, bucket: Dict) -> bool:
209
246
  error_msg = "Failed to retrieve S3 bucket detail"
@@ -240,6 +277,11 @@ def _is_common_exception(e: Exception, bucket: Dict) -> bool:
240
277
  f"{error_msg} for {bucket['Name']} - IllegalLocationConstraintException",
241
278
  )
242
279
  return True
280
+ elif "OwnershipControlsNotFoundError" in e.args[0]:
281
+ logger.warning(
282
+ f"{error_msg} for {bucket['Name']} - OwnershipControlsNotFoundError"
283
+ )
284
+ return True
243
285
  return False
244
286
 
245
287
 
@@ -414,6 +456,29 @@ def _load_s3_public_access_block(
414
456
  )
415
457
 
416
458
 
459
+ @timeit
460
+ def _load_bucket_ownership_controls(
461
+ neo4j_session: neo4j.Session,
462
+ bucket_ownership_controls_configs: List[Dict],
463
+ update_tag: int,
464
+ ) -> None:
465
+ """
466
+ Ingest S3 BucketOwnershipControls results into neo4j.
467
+ """
468
+ ingest_bucket_ownership_controls = """
469
+ UNWIND $bucket_ownership_controls_configs AS bucket_ownership_controls
470
+ MATCH (s:S3Bucket) where s.name = bucket_ownership_controls.bucket
471
+ SET s.object_ownership = bucket_ownership_controls.object_ownership,
472
+ s.lastupdated = $UpdateTag
473
+ """
474
+
475
+ neo4j_session.run(
476
+ ingest_bucket_ownership_controls,
477
+ bucket_ownership_controls_configs=bucket_ownership_controls_configs,
478
+ UpdateTag=update_tag,
479
+ )
480
+
481
+
417
482
  def _set_default_values(neo4j_session: neo4j.Session, aws_account_id: str) -> None:
418
483
  set_defaults = """
419
484
  MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:S3Bucket) where s.anonymous_actions IS NULL
@@ -450,6 +515,7 @@ def load_s3_details(
450
515
  encryption_configs: List[Dict] = []
451
516
  versioning_configs: List[Dict] = []
452
517
  public_access_block_configs: List[Dict] = []
518
+ bucket_ownership_controls_configs: List[Dict] = []
453
519
  for (
454
520
  bucket,
455
521
  acl,
@@ -457,6 +523,7 @@ def load_s3_details(
457
523
  encryption,
458
524
  versioning,
459
525
  public_access_block,
526
+ bucket_ownership_controls,
460
527
  ) in s3_details_iter:
461
528
  parsed_acls = parse_acl(acl, bucket, aws_account_id)
462
529
  if parsed_acls is not None:
@@ -479,6 +546,11 @@ def load_s3_details(
479
546
  )
480
547
  if parsed_public_access_block is not None:
481
548
  public_access_block_configs.append(parsed_public_access_block)
549
+ parsed_bucket_ownership_controls = parse_bucket_ownership_controls(
550
+ bucket, bucket_ownership_controls
551
+ )
552
+ if parsed_bucket_ownership_controls is not None:
553
+ bucket_ownership_controls_configs.append(parsed_bucket_ownership_controls)
482
554
 
483
555
  # cleanup existing policy properties set on S3 Buckets
484
556
  run_cleanup_job(
@@ -494,6 +566,9 @@ def load_s3_details(
494
566
  _load_s3_encryption(neo4j_session, encryption_configs, update_tag)
495
567
  _load_s3_versioning(neo4j_session, versioning_configs, update_tag)
496
568
  _load_s3_public_access_block(neo4j_session, public_access_block_configs, update_tag)
569
+ _load_bucket_ownership_controls(
570
+ neo4j_session, bucket_ownership_controls_configs, update_tag
571
+ )
497
572
  _set_default_values(neo4j_session, aws_account_id)
498
573
 
499
574
 
@@ -751,6 +826,76 @@ def parse_public_access_block(
751
826
  }
752
827
 
753
828
 
829
+ @timeit
830
+ def parse_bucket_ownership_controls(
831
+ bucket: str, bucket_ownership_controls: Optional[Dict]
832
+ ) -> Optional[Dict]:
833
+ """Parses the S3 bucket ownership controls object and returns a dict of the relevant data"""
834
+ # Versioning object JSON looks like:
835
+ # {
836
+ # 'OwnershipControls': {
837
+ # 'Rules': [
838
+ # {
839
+ # 'ObjectOwnership': 'BucketOwnerPreferred'|'ObjectWriter'|'BucketOwnerEnforced'
840
+ # },
841
+ # ]
842
+ # }
843
+ # }
844
+ if bucket_ownership_controls is None:
845
+ return None
846
+ return {
847
+ "bucket": bucket,
848
+ "object_ownership": bucket_ownership_controls.get("OwnershipControls", {})
849
+ .get("Rules", [{}])[0]
850
+ .get("ObjectOwnership"),
851
+ }
852
+
853
+
854
+ @timeit
855
+ def parse_notification_configuration(
856
+ bucket: str, notification_config: Optional[Dict]
857
+ ) -> List[Dict]:
858
+ """
859
+ Parse S3 bucket notification configuration to extract SNS topic notifications.
860
+ Returns a list of notification configurations.
861
+ """
862
+ if not notification_config or "TopicConfigurations" not in notification_config:
863
+ return []
864
+
865
+ notifications = []
866
+ for topic_config in notification_config.get("TopicConfigurations", []):
867
+ notification = {
868
+ "bucket": bucket,
869
+ "TopicArn": topic_config["TopicArn"],
870
+ }
871
+ notifications.append(notification)
872
+ return notifications
873
+
874
+
875
+ @timeit
876
+ def _load_s3_notifications(
877
+ neo4j_session: neo4j.Session,
878
+ notifications: List[Dict],
879
+ update_tag: int,
880
+ ) -> None:
881
+ """
882
+ Ingest S3 bucket to SNS topic notification relationships into neo4j.
883
+ """
884
+ ingest_notifications = """
885
+ UNWIND $notifications AS notification
886
+ MATCH (bucket:S3Bucket{name: notification.bucket})
887
+ MATCH (topic:SNSTopic{arn: notification.TopicArn})
888
+ MERGE (bucket)-[r:NOTIFIES]->(topic)
889
+ ON CREATE SET r.firstseen = timestamp()
890
+ SET r.lastupdated = $UpdateTag
891
+ """
892
+ neo4j_session.run(
893
+ ingest_notifications,
894
+ notifications=notifications,
895
+ UpdateTag=update_tag,
896
+ )
897
+
898
+
754
899
  @timeit
755
900
  def load_s3_buckets(
756
901
  neo4j_session: neo4j.Session,
@@ -811,6 +956,43 @@ def cleanup_s3_bucket_acl_and_policy(
811
956
  )
812
957
 
813
958
 
959
+ @timeit
960
+ @aws_handle_regions
961
+ def _sync_s3_notifications(
962
+ neo4j_session: neo4j.Session,
963
+ boto3_session: boto3.session.Session,
964
+ bucket_data: Dict,
965
+ update_tag: int,
966
+ ) -> None:
967
+ """
968
+ Sync S3 bucket notification configurations to Neo4j.
969
+ """
970
+ logger.info("Syncing S3 bucket notifications")
971
+ s3_client = boto3_session.client("s3")
972
+ notifications = []
973
+
974
+ for bucket in bucket_data["Buckets"]:
975
+ try:
976
+ notification_config = s3_client.get_bucket_notification_configuration(
977
+ Bucket=bucket["Name"]
978
+ )
979
+ parsed_notifications = parse_notification_configuration(
980
+ bucket["Name"], notification_config
981
+ )
982
+ notifications.extend(parsed_notifications)
983
+ logger.debug(
984
+ f"Found {len(parsed_notifications)} notifications for bucket {bucket['Name']}"
985
+ )
986
+ except ClientError as e:
987
+ logger.warning(
988
+ f"Failed to retrieve notification configuration for bucket {bucket['Name']}: {e}"
989
+ )
990
+ continue
991
+
992
+ logger.info(f"Loading {len(notifications)} S3 bucket notifications into Neo4j")
993
+ _load_s3_notifications(neo4j_session, notifications, update_tag)
994
+
995
+
814
996
  @timeit
815
997
  def sync(
816
998
  neo4j_session: neo4j.Session,
@@ -820,9 +1002,16 @@ def sync(
820
1002
  update_tag: int,
821
1003
  common_job_parameters: Dict,
822
1004
  ) -> None:
823
- logger.info("Syncing S3 for account '%s'.", current_aws_account_id)
824
- bucket_data = get_s3_bucket_list(boto3_session)
1005
+ """
1006
+ Sync S3 buckets and their configurations to Neo4j.
1007
+ This includes:
1008
+ 1. Basic bucket information
1009
+ 2. ACLs and policies
1010
+ 3. Notification configurations
1011
+ """
1012
+ logger.info("Syncing S3 for account '%s'", current_aws_account_id)
825
1013
 
1014
+ bucket_data = get_s3_bucket_list(boto3_session)
826
1015
  load_s3_buckets(neo4j_session, bucket_data, current_aws_account_id, update_tag)
827
1016
  cleanup_s3_buckets(neo4j_session, common_job_parameters)
828
1017
 
@@ -835,6 +1024,8 @@ def sync(
835
1024
  )
836
1025
  cleanup_s3_bucket_acl_and_policy(neo4j_session, common_job_parameters)
837
1026
 
1027
+ _sync_s3_notifications(neo4j_session, boto3_session, bucket_data, update_tag)
1028
+
838
1029
  merge_module_sync_metadata(
839
1030
  neo4j_session,
840
1031
  group_type="AWSAccount",
@@ -9,8 +9,10 @@ import boto3
9
9
  import neo4j
10
10
  from botocore.exceptions import ClientError
11
11
 
12
+ from cartography.client.core.tx import load
13
+ from cartography.graph.job import GraphJob
14
+ from cartography.models.aws.sqs.queue import SQSQueueSchema
12
15
  from cartography.util import aws_handle_regions
13
- from cartography.util import run_cleanup_job
14
16
  from cartography.util import timeit
15
17
 
16
18
  logger = logging.getLogger(__name__)
@@ -33,9 +35,7 @@ def get_sqs_queue_attributes(
33
35
  boto3_session: boto3.session.Session,
34
36
  queue_urls: List[str],
35
37
  ) -> List[Tuple[str, Any]]:
36
- """
37
- Iterates over all SQS queues. Returns a dict with url as key, and attributes as value.
38
- """
38
+ """Iterates over all SQS queues and returns a list of (url, attributes)."""
39
39
  client = boto3_session.client("sqs")
40
40
 
41
41
  queue_attributes = []
@@ -58,108 +58,53 @@ def get_sqs_queue_attributes(
58
58
  return queue_attributes
59
59
 
60
60
 
61
- @timeit
62
- def load_sqs_queues(
63
- neo4j_session: neo4j.Session,
64
- data: List[Tuple[str, Any]],
65
- region: str,
66
- current_aws_account_id: str,
67
- aws_update_tag: int,
68
- ) -> None:
69
- ingest_queues = """
70
- UNWIND $Queues as sqs_queue
71
- MERGE (queue:SQSQueue{id: sqs_queue.QueueArn})
72
- ON CREATE SET queue.firstseen = timestamp(), queue.url = sqs_queue.url
73
- SET queue.name = sqs_queue.name, queue.region = $Region, queue.arn = sqs_queue.QueueArn,
74
- queue.created_timestamp = sqs_queue.CreatedTimestamp, queue.delay_seconds = sqs_queue.DelaySeconds,
75
- queue.last_modified_timestamp = sqs_queue.LastModifiedTimestamp,
76
- queue.maximum_message_size = sqs_queue.MaximumMessageSize,
77
- queue.message_retention_period = sqs_queue.MessageRetentionPeriod,
78
- queue.policy = sqs_queue.Policy, queue.arn = sqs_queue.QueueArn,
79
- queue.receive_message_wait_time_seconds = sqs_queue.ReceiveMessageWaitTimeSeconds,
80
- queue.redrive_policy_dead_letter_target_arn = sqs_queue.RedrivePolicy.deadLetterTargetArn,
81
- queue.redrive_policy_max_receive_count = sqs_queue.RedrivePolicy.maxReceiveCount,
82
- queue.visibility_timeout = sqs_queue.VisibilityTimeout,
83
- queue.kms_master_key_id = sqs_queue.KmsMasterKeyId,
84
- queue.kms_data_key_reuse_period_seconds = sqs_queue.KmsDataKeyReusePeriodSeconds,
85
- queue.fifo_queue = sqs_queue.FifoQueue,
86
- queue.content_based_deduplication = sqs_queue.ContentBasedDeduplication,
87
- queue.deduplication_scope = sqs_queue.DeduplicationScope,
88
- queue.fifo_throughput_limit = sqs_queue.FifoThroughputLimit,
89
- queue.lastupdated = $aws_update_tag
90
- WITH queue
91
- MATCH (owner:AWSAccount{id: $AWS_ACCOUNT_ID})
92
- MERGE (owner)-[r:RESOURCE]->(queue)
93
- ON CREATE SET r.firstseen = timestamp()
94
- SET r.lastupdated = $aws_update_tag
95
- """
96
- dead_letter_queues: List[Dict] = []
97
- queues: List[Dict] = []
98
- for url, queue in data:
61
+ def transform_sqs_queues(data: List[Tuple[str, Any]]) -> List[Dict[str, Any]]:
62
+ transformed: List[Dict[str, Any]] = []
63
+ for url, attrs in data:
64
+ queue = dict(attrs)
99
65
  queue["url"] = url
100
- queue["name"] = queue["QueueArn"].split(":")[-1]
101
- queue["CreatedTimestamp"] = int(queue["CreatedTimestamp"])
102
- queue["LastModifiedTimestamp"] = int(queue["LastModifiedTimestamp"])
103
- redrive_policy = queue.get("RedrivePolicy")
66
+ queue["name"] = attrs["QueueArn"].split(":")[-1]
67
+ queue["CreatedTimestamp"] = int(attrs.get("CreatedTimestamp", 0))
68
+ queue["LastModifiedTimestamp"] = int(attrs.get("LastModifiedTimestamp", 0))
69
+ redrive_policy = attrs.get("RedrivePolicy")
104
70
  if redrive_policy:
105
71
  try:
106
72
  rp = json.loads(redrive_policy)
107
73
  except TypeError:
108
74
  rp = {}
109
- queue["RedrivePolicy"] = rp
110
- dead_letter_arn = rp.get("deadLetterTargetArn")
111
- if dead_letter_arn:
112
- dead_letter_queues.append(
113
- {
114
- "arn": queue["QueueArn"],
115
- "dead_letter_arn": dead_letter_arn,
116
- },
117
- )
118
- queues.append(queue)
119
-
120
- neo4j_session.run(
121
- ingest_queues,
122
- Queues=queues,
123
- Region=region,
124
- AWS_ACCOUNT_ID=current_aws_account_id,
125
- aws_update_tag=aws_update_tag,
126
- )
127
-
128
- _attach_dead_letter_queues(neo4j_session, dead_letter_queues, aws_update_tag)
75
+ queue["redrive_policy_dead_letter_target_arn"] = rp.get(
76
+ "deadLetterTargetArn"
77
+ )
78
+ queue["redrive_policy_max_receive_count"] = rp.get("maxReceiveCount")
79
+ transformed.append(queue)
80
+ return transformed
129
81
 
130
82
 
131
83
  @timeit
132
- def _attach_dead_letter_queues(
84
+ def load_sqs_queues(
133
85
  neo4j_session: neo4j.Session,
134
- data: List[Dict[str, str]],
86
+ data: List[Dict[str, Any]],
87
+ region: str,
88
+ current_aws_account_id: str,
135
89
  aws_update_tag: int,
136
90
  ) -> None:
137
- """
138
- Attach deadletter queues to their queues.
139
- """
140
- attach_deadletter_to_queue = """
141
- UNWIND $Relations as relation
142
- MATCH (queue:SQSQueue{id: relation.arn}), (deadletter:SQSQueue{id: relation.dead_letter_arn})
143
- MERGE (queue)-[r:HAS_DEADLETTER_QUEUE]->(deadletter)
144
- ON CREATE SET r.firstseen = timestamp()
145
- SET r.lastupdated = $aws_update_tag
146
- """
147
- neo4j_session.run(
148
- attach_deadletter_to_queue,
149
- Relations=data,
150
- aws_update_tag=aws_update_tag,
91
+ load(
92
+ neo4j_session,
93
+ SQSQueueSchema(),
94
+ data,
95
+ lastupdated=aws_update_tag,
96
+ Region=region,
97
+ AWS_ID=current_aws_account_id,
151
98
  )
152
99
 
153
100
 
154
101
  @timeit
155
102
  def cleanup_sqs_queues(
156
103
  neo4j_session: neo4j.Session,
157
- common_job_parameters: Dict,
104
+ common_job_parameters: Dict[str, Any],
158
105
  ) -> None:
159
- run_cleanup_job(
160
- "aws_import_sqs_queues_cleanup.json",
161
- neo4j_session,
162
- common_job_parameters,
106
+ GraphJob.from_node_schema(SQSQueueSchema(), common_job_parameters).run(
107
+ neo4j_session
163
108
  )
164
109
 
165
110
 
@@ -170,7 +115,7 @@ def sync(
170
115
  regions: List[str],
171
116
  current_aws_account_id: str,
172
117
  update_tag: int,
173
- common_job_parameters: Dict,
118
+ common_job_parameters: Dict[str, Any],
174
119
  ) -> None:
175
120
  for region in regions:
176
121
  logger.info(
@@ -179,12 +124,13 @@ def sync(
179
124
  current_aws_account_id,
180
125
  )
181
126
  queue_urls = get_sqs_queue_list(boto3_session, region)
182
- if len(queue_urls) == 0:
127
+ if not queue_urls:
183
128
  continue
184
129
  queue_attributes = get_sqs_queue_attributes(boto3_session, queue_urls)
130
+ transformed = transform_sqs_queues(queue_attributes)
185
131
  load_sqs_queues(
186
132
  neo4j_session,
187
- queue_attributes,
133
+ transformed,
188
134
  region,
189
135
  current_aws_account_id,
190
136
  update_tag,
@@ -4,6 +4,8 @@ import logging
4
4
  import neo4j
5
5
 
6
6
  from cartography.config import Config
7
+ from cartography.intel.entra.applications import sync_entra_applications
8
+ from cartography.intel.entra.groups import sync_entra_groups
7
9
  from cartography.intel.entra.ou import sync_entra_ous
8
10
  from cartography.intel.entra.users import sync_entra_users
9
11
  from cartography.util import timeit
@@ -47,6 +49,16 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
47
49
  common_job_parameters,
48
50
  )
49
51
 
52
+ # Run group sync
53
+ await sync_entra_groups(
54
+ neo4j_session,
55
+ config.entra_tenant_id,
56
+ config.entra_client_id,
57
+ config.entra_client_secret,
58
+ config.update_tag,
59
+ common_job_parameters,
60
+ )
61
+
50
62
  # Run OU sync
51
63
  await sync_entra_ous(
52
64
  neo4j_session,
@@ -57,5 +69,15 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
57
69
  common_job_parameters,
58
70
  )
59
71
 
72
+ # Run application sync
73
+ await sync_entra_applications(
74
+ neo4j_session,
75
+ config.entra_tenant_id,
76
+ config.entra_client_id,
77
+ config.entra_client_secret,
78
+ config.update_tag,
79
+ common_job_parameters,
80
+ )
81
+
60
82
  # Execute both syncs in sequence
61
83
  asyncio.run(main())