cartography 0.108.0rc1__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 (81) 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.py +17 -4
  7. cartography/intel/aws/cloudtrail_management_events.py +614 -16
  8. cartography/intel/aws/cloudwatch.py +73 -4
  9. cartography/intel/aws/ec2/subnets.py +37 -63
  10. cartography/intel/aws/ecr.py +55 -80
  11. cartography/intel/aws/elasticache.py +102 -79
  12. cartography/intel/aws/eventbridge.py +91 -0
  13. cartography/intel/aws/glue.py +117 -0
  14. cartography/intel/aws/identitycenter.py +71 -23
  15. cartography/intel/aws/kms.py +160 -200
  16. cartography/intel/aws/lambda_function.py +206 -190
  17. cartography/intel/aws/rds.py +243 -458
  18. cartography/intel/aws/resourcegroupstaggingapi.py +77 -18
  19. cartography/intel/aws/resources.py +4 -0
  20. cartography/intel/aws/route53.py +334 -332
  21. cartography/intel/aws/secretsmanager.py +62 -44
  22. cartography/intel/entra/groups.py +29 -1
  23. cartography/intel/gcp/__init__.py +10 -0
  24. cartography/intel/gcp/compute.py +19 -42
  25. cartography/intel/trivy/__init__.py +73 -13
  26. cartography/intel/trivy/scanner.py +115 -92
  27. cartography/models/aws/cloudtrail/management_events.py +95 -6
  28. cartography/models/aws/cloudtrail/trail.py +21 -0
  29. cartography/models/aws/cloudwatch/metric_alarm.py +53 -0
  30. cartography/models/aws/ec2/subnets.py +65 -0
  31. cartography/models/aws/ecr/__init__.py +0 -0
  32. cartography/models/aws/ecr/image.py +41 -0
  33. cartography/models/aws/ecr/repository.py +72 -0
  34. cartography/models/aws/ecr/repository_image.py +95 -0
  35. cartography/models/aws/elasticache/__init__.py +0 -0
  36. cartography/models/aws/elasticache/cluster.py +65 -0
  37. cartography/models/aws/elasticache/topic.py +67 -0
  38. cartography/models/aws/eventbridge/__init__.py +0 -0
  39. cartography/models/aws/eventbridge/rule.py +77 -0
  40. cartography/models/aws/glue/__init__.py +0 -0
  41. cartography/models/aws/glue/connection.py +51 -0
  42. cartography/models/aws/identitycenter/awspermissionset.py +44 -0
  43. cartography/models/aws/kms/__init__.py +0 -0
  44. cartography/models/aws/kms/aliases.py +86 -0
  45. cartography/models/aws/kms/grants.py +65 -0
  46. cartography/models/aws/kms/keys.py +88 -0
  47. cartography/models/aws/lambda_function/__init__.py +0 -0
  48. cartography/models/aws/lambda_function/alias.py +74 -0
  49. cartography/models/aws/lambda_function/event_source_mapping.py +88 -0
  50. cartography/models/aws/lambda_function/lambda_function.py +89 -0
  51. cartography/models/aws/lambda_function/layer.py +72 -0
  52. cartography/models/aws/rds/__init__.py +0 -0
  53. cartography/models/aws/rds/cluster.py +89 -0
  54. cartography/models/aws/rds/instance.py +154 -0
  55. cartography/models/aws/rds/snapshot.py +108 -0
  56. cartography/models/aws/rds/subnet_group.py +101 -0
  57. cartography/models/aws/route53/__init__.py +0 -0
  58. cartography/models/aws/route53/dnsrecord.py +214 -0
  59. cartography/models/aws/route53/nameserver.py +63 -0
  60. cartography/models/aws/route53/subzone.py +40 -0
  61. cartography/models/aws/route53/zone.py +47 -0
  62. cartography/models/aws/secretsmanager/secret.py +106 -0
  63. cartography/models/entra/group.py +26 -0
  64. cartography/models/entra/user.py +6 -0
  65. cartography/models/gcp/compute/__init__.py +0 -0
  66. cartography/models/gcp/compute/vpc.py +50 -0
  67. cartography/util.py +8 -1
  68. {cartography-0.108.0rc1.dist-info → cartography-0.109.0.dist-info}/METADATA +2 -2
  69. {cartography-0.108.0rc1.dist-info → cartography-0.109.0.dist-info}/RECORD +73 -44
  70. cartography/data/jobs/cleanup/aws_dns_cleanup.json +0 -65
  71. cartography/data/jobs/cleanup/aws_import_identity_center_cleanup.json +0 -16
  72. cartography/data/jobs/cleanup/aws_import_lambda_cleanup.json +0 -50
  73. cartography/data/jobs/cleanup/aws_import_rds_clusters_cleanup.json +0 -23
  74. cartography/data/jobs/cleanup/aws_import_rds_instances_cleanup.json +0 -47
  75. cartography/data/jobs/cleanup/aws_import_rds_snapshots_cleanup.json +0 -23
  76. cartography/data/jobs/cleanup/aws_import_secrets_cleanup.json +0 -8
  77. cartography/data/jobs/cleanup/aws_kms_details.json +0 -10
  78. {cartography-0.108.0rc1.dist-info → cartography-0.109.0.dist-info}/WHEEL +0 -0
  79. {cartography-0.108.0rc1.dist-info → cartography-0.109.0.dist-info}/entry_points.txt +0 -0
  80. {cartography-0.108.0rc1.dist-info → cartography-0.109.0.dist-info}/licenses/LICENSE +0 -0
  81. {cartography-0.108.0rc1.dist-info → cartography-0.109.0.dist-info}/top_level.txt +0 -0
@@ -1,118 +1,132 @@
1
1
  import logging
2
- from typing import Dict
3
- from typing import List
4
- from typing import Set
2
+ from typing import Any
5
3
 
6
4
  import boto3
7
5
  import neo4j
8
6
 
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.models.aws.elasticache.cluster import ElasticacheClusterSchema
10
+ from cartography.models.aws.elasticache.topic import ElasticacheTopicSchema
9
11
  from cartography.stats import get_stats_client
10
12
  from cartography.util import aws_handle_regions
11
13
  from cartography.util import merge_module_sync_metadata
12
- from cartography.util import run_cleanup_job
13
14
  from cartography.util import timeit
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
  stat_handler = get_stats_client(__name__)
17
18
 
18
19
 
19
- def _get_topic(cluster: Dict) -> Dict:
20
- return cluster["NotificationConfiguration"]
21
-
22
-
23
- def transform_elasticache_topics(cluster_data: List[Dict]) -> List[Dict]:
24
- """
25
- Collect unique TopicArns from the cluster data
26
- """
27
- seen: Set[str] = set()
28
- topics: List[Dict] = []
29
- for cluster in cluster_data:
30
- topic = _get_topic(cluster)
31
- topic_arn = topic["TopicArn"]
32
- if topic_arn not in seen:
33
- seen.add(topic_arn)
34
- topics.append(topic)
35
- return topics
36
-
37
-
38
20
  @timeit
39
21
  @aws_handle_regions
40
22
  def get_elasticache_clusters(
41
23
  boto3_session: boto3.session.Session,
42
24
  region: str,
43
- ) -> List[Dict]:
44
- logger.debug(f"Getting ElastiCache Clusters in region '{region}'.")
25
+ ) -> list[dict[str, Any]]:
45
26
  client = boto3_session.client("elasticache", region_name=region)
46
27
  paginator = client.get_paginator("describe_cache_clusters")
47
- clusters: List[Dict] = []
28
+ clusters: list[dict[str, Any]] = []
48
29
  for page in paginator.paginate():
49
- clusters.extend(page["CacheClusters"])
30
+ clusters.extend(page.get("CacheClusters", []))
50
31
  return clusters
51
32
 
52
33
 
34
+ def transform_elasticache_clusters(
35
+ clusters: list[dict[str, Any]], region: str
36
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
37
+ cluster_data: list[dict[str, Any]] = []
38
+ topics: dict[str, dict[str, Any]] = {}
39
+
40
+ for cluster in clusters:
41
+ notification = cluster.get("NotificationConfiguration", {})
42
+ topic_arn = notification.get("TopicArn")
43
+ cluster_record = {
44
+ "ARN": cluster["ARN"],
45
+ "CacheClusterId": cluster["CacheClusterId"],
46
+ "CacheNodeType": cluster.get("CacheNodeType"),
47
+ "Engine": cluster.get("Engine"),
48
+ "EngineVersion": cluster.get("EngineVersion"),
49
+ "CacheClusterStatus": cluster.get("CacheClusterStatus"),
50
+ "NumCacheNodes": cluster.get("NumCacheNodes"),
51
+ "PreferredAvailabilityZone": cluster.get("PreferredAvailabilityZone"),
52
+ "PreferredMaintenanceWindow": cluster.get("PreferredMaintenanceWindow"),
53
+ "CacheClusterCreateTime": cluster.get("CacheClusterCreateTime"),
54
+ "CacheSubnetGroupName": cluster.get("CacheSubnetGroupName"),
55
+ "AutoMinorVersionUpgrade": cluster.get("AutoMinorVersionUpgrade"),
56
+ "ReplicationGroupId": cluster.get("ReplicationGroupId"),
57
+ "SnapshotRetentionLimit": cluster.get("SnapshotRetentionLimit"),
58
+ "SnapshotWindow": cluster.get("SnapshotWindow"),
59
+ "AuthTokenEnabled": cluster.get("AuthTokenEnabled"),
60
+ "TransitEncryptionEnabled": cluster.get("TransitEncryptionEnabled"),
61
+ "AtRestEncryptionEnabled": cluster.get("AtRestEncryptionEnabled"),
62
+ "TopicArn": topic_arn,
63
+ "Region": region,
64
+ }
65
+ cluster_data.append(cluster_record)
66
+
67
+ if topic_arn:
68
+ topics.setdefault(
69
+ topic_arn,
70
+ {
71
+ "TopicArn": topic_arn,
72
+ "TopicStatus": notification.get("TopicStatus"),
73
+ "cluster_arns": [],
74
+ },
75
+ )["cluster_arns"].append(cluster["ARN"])
76
+
77
+ return cluster_data, list(topics.values())
78
+
79
+
53
80
  @timeit
54
81
  def load_elasticache_clusters(
55
82
  neo4j_session: neo4j.Session,
56
- clusters: List[Dict],
83
+ clusters: list[dict[str, Any]],
57
84
  region: str,
58
85
  aws_account_id: str,
59
86
  update_tag: int,
60
87
  ) -> None:
61
- query = """
62
- UNWIND $clusters as elasticache_cluster
63
- MERGE (cluster:ElasticacheCluster{id:elasticache_cluster.ARN})
64
- ON CREATE SET cluster.firstseen = timestamp(),
65
- cluster.arn = elasticache_cluster.ARN,
66
- cluster.topic_arn = elasticache_cluster.NotificationConfiguration.TopicArn,
67
- cluster.id = elasticache_cluster.CacheClusterId,
68
- cluster.region = $region
69
- SET cluster.lastupdated = $aws_update_tag
70
-
71
- WITH cluster, elasticache_cluster
72
- MATCH (owner:AWSAccount{id: $aws_account_id})
73
- MERGE (owner)-[r3:RESOURCE]->(cluster)
74
- ON CREATE SET r3.firstseen = timestamp()
75
- SET r3.lastupdated = $aws_update_tag
76
-
77
- WITH elasticache_cluster, owner
78
- WHERE NOT elasticache_cluster.NotificationConfiguration IS NULL
79
- MERGE (topic:ElasticacheTopic{id: elasticache_cluster.NotificationConfiguration.TopicArn})
80
- ON CREATE SET topic.firstseen = timestamp(),
81
- topic.arn = elasticache_cluster.NotificationConfiguration.TopicArn
82
- SET topic.lastupdated = $aws_update_tag,
83
- topic.status = elasticache_cluster.NotificationConfiguration.Status
84
-
85
- MERGE (topic)-[r:CACHE_CLUSTER]->(cluster)
86
- ON CREATE SET r.firstseen = timestamp()
87
- SET r.lastupdated = $aws_update_tag
88
- WITH cluster, topic
89
-
90
- MERGE (owner)-[r2:RESOURCE]->(topic)
91
- ON CREATE SET r2.firstseen = timestamp()
92
- SET r2.lastupdated = $aws_update_tag
93
- """
94
88
  logger.info(
95
- f"Loading f{len(clusters)} ElastiCache clusters for region '{region}' into graph.",
89
+ f"Loading {len(clusters)} ElastiCache clusters for region '{region}' into graph."
96
90
  )
97
- neo4j_session.run(
98
- query,
99
- clusters=clusters,
100
- region=region,
101
- aws_update_tag=update_tag,
102
- aws_account_id=aws_account_id,
91
+ load(
92
+ neo4j_session,
93
+ ElasticacheClusterSchema(),
94
+ clusters,
95
+ lastupdated=update_tag,
96
+ Region=region,
97
+ AWS_ID=aws_account_id,
103
98
  )
104
99
 
105
100
 
106
101
  @timeit
107
- def cleanup(
102
+ def load_elasticache_topics(
108
103
  neo4j_session: neo4j.Session,
109
- current_aws_account_id: str,
104
+ topics: list[dict[str, Any]],
105
+ aws_account_id: str,
110
106
  update_tag: int,
111
107
  ) -> None:
112
- run_cleanup_job(
113
- "aws_import_elasticache_cleanup.json",
108
+ if not topics:
109
+ return
110
+ logger.info(f"Loading {len(topics)} ElastiCache topics into graph.")
111
+ load(
114
112
  neo4j_session,
115
- {"UPDATE_TAG": update_tag, "AWS_ID": current_aws_account_id},
113
+ ElasticacheTopicSchema(),
114
+ topics,
115
+ lastupdated=update_tag,
116
+ AWS_ID=aws_account_id,
117
+ )
118
+
119
+
120
+ @timeit
121
+ def cleanup(
122
+ neo4j_session: neo4j.Session,
123
+ common_job_parameters: dict[str, Any],
124
+ ) -> None:
125
+ GraphJob.from_node_schema(ElasticacheClusterSchema(), common_job_parameters).run(
126
+ neo4j_session
127
+ )
128
+ GraphJob.from_node_schema(ElasticacheTopicSchema(), common_job_parameters).run(
129
+ neo4j_session
116
130
  )
117
131
 
118
132
 
@@ -120,24 +134,33 @@ def cleanup(
120
134
  def sync(
121
135
  neo4j_session: neo4j.Session,
122
136
  boto3_session: boto3.session.Session,
123
- regions: List[str],
137
+ regions: list[str],
124
138
  current_aws_account_id: str,
125
139
  update_tag: int,
126
- common_job_parameters: Dict,
140
+ common_job_parameters: dict[str, Any],
127
141
  ) -> None:
128
142
  for region in regions:
129
143
  logger.info(
130
- f"Syncing ElastiCache clusters for region '{region}' in account {current_aws_account_id}",
144
+ "Syncing ElastiCache clusters for region '%s' in account '%s'.",
145
+ region,
146
+ current_aws_account_id,
131
147
  )
132
- clusters = get_elasticache_clusters(boto3_session, region)
148
+ raw_clusters = get_elasticache_clusters(boto3_session, region)
149
+ cluster_data, topic_data = transform_elasticache_clusters(raw_clusters, region)
133
150
  load_elasticache_clusters(
134
151
  neo4j_session,
135
- clusters,
152
+ cluster_data,
136
153
  region,
137
154
  current_aws_account_id,
138
155
  update_tag,
139
156
  )
140
- cleanup(neo4j_session, current_aws_account_id, update_tag)
157
+ load_elasticache_topics(
158
+ neo4j_session,
159
+ topic_data,
160
+ current_aws_account_id,
161
+ update_tag,
162
+ )
163
+ cleanup(neo4j_session, common_job_parameters)
141
164
  merge_module_sync_metadata(
142
165
  neo4j_session,
143
166
  group_type="AWSAccount",
@@ -0,0 +1,91 @@
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.eventbridge.rule import EventBridgeRuleSchema
13
+ from cartography.util import aws_handle_regions
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ @aws_handle_regions
21
+ def get_eventbridge_rules(
22
+ boto3_session: boto3.Session, region: str
23
+ ) -> List[Dict[str, Any]]:
24
+ client = boto3_session.client(
25
+ "events", region_name=region, config=get_botocore_config()
26
+ )
27
+ paginator = client.get_paginator("list_rules")
28
+ rules = []
29
+
30
+ for page in paginator.paginate():
31
+ rules.extend(page.get("Rules", []))
32
+
33
+ return rules
34
+
35
+
36
+ @timeit
37
+ def load_eventbridge_rules(
38
+ neo4j_session: neo4j.Session,
39
+ data: List[Dict[str, Any]],
40
+ region: str,
41
+ current_aws_account_id: str,
42
+ aws_update_tag: int,
43
+ ) -> None:
44
+ logger.info(
45
+ f"Loading EventBridge {len(data)} rules for region '{region}' into graph.",
46
+ )
47
+ load(
48
+ neo4j_session,
49
+ EventBridgeRuleSchema(),
50
+ data,
51
+ lastupdated=aws_update_tag,
52
+ Region=region,
53
+ AWS_ID=current_aws_account_id,
54
+ )
55
+
56
+
57
+ @timeit
58
+ def cleanup(
59
+ neo4j_session: neo4j.Session,
60
+ common_job_parameters: Dict[str, Any],
61
+ ) -> None:
62
+ logger.debug("Running EventBridge cleanup job.")
63
+ GraphJob.from_node_schema(EventBridgeRuleSchema(), common_job_parameters).run(
64
+ neo4j_session
65
+ )
66
+
67
+
68
+ @timeit
69
+ def sync(
70
+ neo4j_session: neo4j.Session,
71
+ boto3_session: boto3.session.Session,
72
+ regions: List[str],
73
+ current_aws_account_id: str,
74
+ update_tag: int,
75
+ common_job_parameters: Dict[str, Any],
76
+ ) -> None:
77
+ for region in regions:
78
+ logger.info(
79
+ f"Syncing EventBridge for region '{region}' in account '{current_aws_account_id}'.",
80
+ )
81
+
82
+ rules = get_eventbridge_rules(boto3_session, region)
83
+ load_eventbridge_rules(
84
+ neo4j_session,
85
+ rules,
86
+ region,
87
+ current_aws_account_id,
88
+ update_tag,
89
+ )
90
+
91
+ cleanup(neo4j_session, common_job_parameters)
@@ -0,0 +1,117 @@
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.glue.connection import GlueConnectionSchema
13
+ from cartography.util import aws_handle_regions
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ @aws_handle_regions
21
+ def get_glue_connections(
22
+ boto3_session: boto3.Session, region: str
23
+ ) -> List[Dict[str, Any]]:
24
+ client = boto3_session.client(
25
+ "glue", region_name=region, config=get_botocore_config()
26
+ )
27
+ paginator = client.get_paginator("get_connections")
28
+ connections = []
29
+ for page in paginator.paginate():
30
+ connections.extend(page.get("ConnectionList", []))
31
+
32
+ return connections
33
+
34
+
35
+ def transform_glue_connections(
36
+ connections: List[Dict[str, Any]], region: str
37
+ ) -> List[Dict[str, Any]]:
38
+ """
39
+ Transform Glue connection data for ingestion
40
+ """
41
+ transformed_connections = []
42
+ for connection in connections:
43
+ transformed_connection = {
44
+ "Name": connection["Name"],
45
+ "Description": connection.get("Description"),
46
+ "ConnectionType": connection.get("ConnectionType"),
47
+ "Status": connection.get("Status"),
48
+ "StatusReason": connection.get("StatusReason"),
49
+ "AuthenticationType": connection.get("AuthenticationConfiguration", {}).get(
50
+ "AuthenticationType"
51
+ ),
52
+ "SecretArn": connection.get("AuthenticationConfiguration", {}).get(
53
+ "SecretArn"
54
+ ),
55
+ "Region": region,
56
+ }
57
+ transformed_connections.append(transformed_connection)
58
+ return transformed_connections
59
+
60
+
61
+ @timeit
62
+ def load_glue_connections(
63
+ neo4j_session: neo4j.Session,
64
+ data: List[Dict[str, Any]],
65
+ region: str,
66
+ current_aws_account_id: str,
67
+ aws_update_tag: int,
68
+ ) -> None:
69
+ logger.info(
70
+ f"Loading Glue {len(data)} connections for region '{region}' into graph.",
71
+ )
72
+ load(
73
+ neo4j_session,
74
+ GlueConnectionSchema(),
75
+ data,
76
+ lastupdated=aws_update_tag,
77
+ Region=region,
78
+ AWS_ID=current_aws_account_id,
79
+ )
80
+
81
+
82
+ @timeit
83
+ def cleanup(
84
+ neo4j_session: neo4j.Session,
85
+ common_job_parameters: Dict[str, Any],
86
+ ) -> None:
87
+ logger.debug("Running Glue cleanup job.")
88
+ GraphJob.from_node_schema(GlueConnectionSchema(), common_job_parameters).run(
89
+ neo4j_session
90
+ )
91
+
92
+
93
+ @timeit
94
+ def sync(
95
+ neo4j_session: neo4j.Session,
96
+ boto3_session: boto3.session.Session,
97
+ regions: List[str],
98
+ current_aws_account_id: str,
99
+ update_tag: int,
100
+ common_job_parameters: Dict[str, Any],
101
+ ) -> None:
102
+ for region in regions:
103
+ logger.info(
104
+ f"Syncing Glue for region '{region}' in account '{current_aws_account_id}'.",
105
+ )
106
+
107
+ connections = get_glue_connections(boto3_session, region)
108
+ transformed_connections = transform_glue_connections(connections, region)
109
+ load_glue_connections(
110
+ neo4j_session,
111
+ transformed_connections,
112
+ region,
113
+ current_aws_account_id,
114
+ update_tag,
115
+ )
116
+
117
+ cleanup(neo4j_session, common_job_parameters)
@@ -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