cartography 0.101.1rc2__py3-none-any.whl → 0.102.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.

@@ -0,0 +1,287 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import boto3
5
+ import neo4j
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.intel.aws.ec2.util import get_botocore_config
10
+ from cartography.models.aws.ec2.route_table_associations import RouteTableAssociationSchema
11
+ from cartography.models.aws.ec2.route_tables import RouteTableSchema
12
+ from cartography.models.aws.ec2.routes import RouteSchema
13
+ from cartography.util import aws_handle_regions
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _get_route_id_and_target(route_table_id: str, route: dict[str, Any]) -> tuple[str, str | None]:
20
+ """
21
+ Generate a unique identifier for an AWS EC2 route and return the target of the route
22
+ regardless of its type.
23
+
24
+ Args:
25
+ route_table_id: The ID of the route table this route belongs to
26
+ route: The route data from AWS API
27
+
28
+ Returns:
29
+ A tuple containing the unique identifier for the route and the target of the route
30
+ """
31
+ route_target_keys = [
32
+ 'DestinationCidrBlock',
33
+ 'DestinationIpv6CidrBlock',
34
+ 'GatewayId',
35
+ 'InstanceId',
36
+ 'NatGatewayId',
37
+ 'TransitGatewayId',
38
+ 'LocalGatewayId',
39
+ 'CarrierGatewayId',
40
+ 'NetworkInterfaceId',
41
+ 'VpcPeeringConnectionId',
42
+ 'EgressOnlyInternetGatewayId',
43
+ 'CoreNetworkArn',
44
+ ]
45
+
46
+ # Start with the route table ID
47
+ parts = [route_table_id]
48
+ target = None
49
+ found_target = False
50
+
51
+ for key in route_target_keys:
52
+ # Each route is a "union"-like data structure, so only one of the keys will be present.
53
+ if key in route:
54
+ parts.append(route[key])
55
+ target = route[key]
56
+ found_target = True
57
+ break
58
+
59
+ if not found_target:
60
+ logger.warning(
61
+ f"No target found for route in {route_table_id}. Please review the route and file an issue to "
62
+ "https://github.com/cartography-cncf/cartography/issues sharing what the route table looks like "
63
+ "so that we can update the available keys.",
64
+ )
65
+
66
+ return '|'.join(parts), target
67
+
68
+
69
+ @timeit
70
+ @aws_handle_regions
71
+ def get_route_tables(boto3_session: boto3.session.Session, region: str) -> list[dict[str, Any]]:
72
+ client = boto3_session.client('ec2', region_name=region, config=get_botocore_config())
73
+ paginator = client.get_paginator('describe_route_tables')
74
+ route_tables: list[dict[str, Any]] = []
75
+ for page in paginator.paginate():
76
+ route_tables.extend(page['RouteTables'])
77
+ return route_tables
78
+
79
+
80
+ def _transform_route_table_associations(
81
+ route_table_id: str,
82
+ associations: list[dict[str, Any]],
83
+ ) -> tuple[list[dict[str, Any]], bool]:
84
+ """
85
+ Transform route table association data into a format suitable for cartography ingestion.
86
+
87
+ Args:
88
+ route_table_id: The ID of the route table
89
+ associations: List of association data from AWS API
90
+
91
+ Returns:
92
+ 1. List of transformed association data
93
+ 2. Boolean indicating if the association is the main association, meaning that the route table is the main
94
+ route table for the VPC
95
+ """
96
+ transformed = []
97
+ is_main = False
98
+ for association in associations:
99
+ if association.get('SubnetId'):
100
+ target = association['SubnetId']
101
+ elif association.get('GatewayId'):
102
+ target = association['GatewayId']
103
+ else:
104
+ is_main = True
105
+ target = 'main'
106
+
107
+ transformed_association = {
108
+ 'id': association['RouteTableAssociationId'],
109
+ 'route_table_id': route_table_id,
110
+ 'subnet_id': association.get('SubnetId'),
111
+ 'gateway_id': association.get('GatewayId'),
112
+ 'main': association.get('Main', False),
113
+ 'association_state': association.get('AssociationState', {}).get('State'),
114
+ 'association_state_message': association.get('AssociationState', {}).get('Message'),
115
+ '_target': target,
116
+ }
117
+ transformed.append(transformed_association)
118
+ return transformed, is_main
119
+
120
+
121
+ def _transform_route_table_routes(route_table_id: str, routes: list[dict[str, Any]]) -> list[dict[str, Any]]:
122
+ """
123
+ Transform route table route data into a format suitable for cartography ingestion.
124
+
125
+ Args:
126
+ route_table_id: The ID of the route table
127
+ routes: List of route data from AWS API
128
+
129
+ Returns:
130
+ List of transformed route data
131
+ """
132
+ transformed = []
133
+ for route in routes:
134
+ route_id, target = _get_route_id_and_target(route_table_id, route)
135
+
136
+ transformed_route = {
137
+ 'id': route_id,
138
+ 'route_table_id': route_table_id,
139
+ 'destination_cidr_block': route.get('DestinationCidrBlock'),
140
+ 'destination_ipv6_cidr_block': route.get('DestinationIpv6CidrBlock'),
141
+ 'gateway_id': route.get('GatewayId'),
142
+ 'instance_id': route.get('InstanceId'),
143
+ 'instance_owner_id': route.get('InstanceOwnerId'),
144
+ 'nat_gateway_id': route.get('NatGatewayId'),
145
+ 'transit_gateway_id': route.get('TransitGatewayId'),
146
+ 'local_gateway_id': route.get('LocalGatewayId'),
147
+ 'carrier_gateway_id': route.get('CarrierGatewayId'),
148
+ 'network_interface_id': route.get('NetworkInterfaceId'),
149
+ 'vpc_peering_connection_id': route.get('VpcPeeringConnectionId'),
150
+ 'state': route.get('State'),
151
+ 'origin': route.get('Origin'),
152
+ 'core_network_arn': route.get('CoreNetworkArn'),
153
+ 'destination_prefix_list_id': route.get('DestinationPrefixListId'),
154
+ 'egress_only_internet_gateway_id': route.get('EgressOnlyInternetGatewayId'),
155
+ '_target': target,
156
+ }
157
+ transformed.append(transformed_route)
158
+ return transformed
159
+
160
+
161
+ def transform_route_table_data(
162
+ route_tables: list[dict[str, Any]],
163
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
164
+ """
165
+ Transform route table data into a format suitable for cartography ingestion.
166
+
167
+ Args:
168
+ route_tables: List of route table data from AWS API
169
+
170
+ Returns:
171
+ Tuple of (transformed route table data, transformed association data, transformed route data)
172
+ """
173
+ transformed_tables = []
174
+ association_data = []
175
+ route_data = []
176
+
177
+ for rt in route_tables:
178
+ route_table_id = rt['RouteTableId']
179
+
180
+ # Transform routes
181
+ current_routes = []
182
+ if rt.get('Routes'):
183
+ current_routes = _transform_route_table_routes(route_table_id, rt['Routes'])
184
+ route_data.extend(current_routes)
185
+
186
+ # If the rt has a association marked with main=True, then it is the main route table for the VPC.
187
+ is_main = False
188
+ # Transform associations
189
+ if rt.get('Associations'):
190
+ associations, is_main = _transform_route_table_associations(route_table_id, rt['Associations'])
191
+ association_data.extend(associations)
192
+
193
+ transformed_rt = {
194
+ 'id': route_table_id,
195
+ 'route_table_id': route_table_id,
196
+ 'owner_id': rt.get('OwnerId'),
197
+ 'vpc_id': rt.get('VpcId'),
198
+ 'VpnGatewayIds': [vgw['GatewayId'] for vgw in rt.get('PropagatingVgws', [])],
199
+ 'RouteTableAssociationIds': [assoc['RouteTableAssociationId'] for assoc in rt.get('Associations', [])],
200
+ 'RouteIds': [route['id'] for route in current_routes],
201
+ 'tags': rt.get('Tags', []),
202
+ 'main': is_main,
203
+ }
204
+ transformed_tables.append(transformed_rt)
205
+
206
+ return transformed_tables, association_data, route_data
207
+
208
+
209
+ @timeit
210
+ def load_route_tables(
211
+ neo4j_session: neo4j.Session,
212
+ data: list[dict[str, Any]],
213
+ region: str,
214
+ current_aws_account_id: str,
215
+ update_tag: int,
216
+ ) -> None:
217
+ load(
218
+ neo4j_session,
219
+ RouteTableSchema(),
220
+ data,
221
+ Region=region,
222
+ AWS_ID=current_aws_account_id,
223
+ lastupdated=update_tag,
224
+ )
225
+
226
+
227
+ @timeit
228
+ def load_route_table_associations(
229
+ neo4j_session: neo4j.Session,
230
+ data: list[dict[str, Any]],
231
+ region: str,
232
+ current_aws_account_id: str,
233
+ update_tag: int,
234
+ ) -> None:
235
+ load(
236
+ neo4j_session,
237
+ RouteTableAssociationSchema(),
238
+ data,
239
+ Region=region,
240
+ AWS_ID=current_aws_account_id,
241
+ lastupdated=update_tag,
242
+ )
243
+
244
+
245
+ @timeit
246
+ def load_routes(
247
+ neo4j_session: neo4j.Session,
248
+ data: list[dict[str, Any]],
249
+ region: str,
250
+ current_aws_account_id: str,
251
+ update_tag: int,
252
+ ) -> None:
253
+ load(
254
+ neo4j_session,
255
+ RouteSchema(),
256
+ data,
257
+ Region=region,
258
+ AWS_ID=current_aws_account_id,
259
+ lastupdated=update_tag,
260
+ )
261
+
262
+
263
+ @timeit
264
+ def cleanup(neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]) -> None:
265
+ logger.debug("Running EC2 route tables cleanup")
266
+ GraphJob.from_node_schema(RouteTableSchema(), common_job_parameters).run(neo4j_session)
267
+ GraphJob.from_node_schema(RouteSchema(), common_job_parameters).run(neo4j_session)
268
+ GraphJob.from_node_schema(RouteTableAssociationSchema(), common_job_parameters).run(neo4j_session)
269
+
270
+
271
+ @timeit
272
+ def sync_route_tables(
273
+ neo4j_session: neo4j.Session,
274
+ boto3_session: boto3.session.Session,
275
+ regions: list[str],
276
+ current_aws_account_id: str,
277
+ update_tag: int,
278
+ common_job_parameters: dict[str, Any],
279
+ ) -> None:
280
+ for region in regions:
281
+ logger.info("Syncing EC2 route tables for region '%s' in account '%s'.", region, current_aws_account_id)
282
+ route_tables = get_route_tables(boto3_session, region)
283
+ transformed_tables, association_data, route_data = transform_route_table_data(route_tables)
284
+ load_routes(neo4j_session, route_data, region, current_aws_account_id, update_tag)
285
+ load_route_table_associations(neo4j_session, association_data, region, current_aws_account_id, update_tag)
286
+ load_route_tables(neo4j_session, transformed_tables, region, current_aws_account_id, update_tag)
287
+ cleanup(neo4j_session, common_job_parameters)
@@ -45,6 +45,7 @@ from .ec2.volumes import sync_ebs_volumes
45
45
  from .ec2.vpc import sync_vpc
46
46
  from .ec2.vpc_peerings import sync_vpc_peerings
47
47
  from .iam_instance_profiles import sync_iam_instance_profiles
48
+ from cartography.intel.aws.ec2.route_tables import sync_route_tables
48
49
 
49
50
  RESOURCE_FUNCTIONS: Dict[str, Callable[..., None]] = {
50
51
  'iam': iam.sync,
@@ -62,6 +63,7 @@ RESOURCE_FUNCTIONS: Dict[str, Callable[..., None]] = {
62
63
  'ec2:load_balancer_v2': sync_load_balancer_v2s,
63
64
  'ec2:network_acls': sync_network_acls,
64
65
  'ec2:network_interface': sync_network_interfaces,
66
+ 'ec2:route_table': sync_route_tables,
65
67
  'ec2:security_group': sync_ec2_security_groupinfo,
66
68
  'ec2:subnet': sync_subnets,
67
69
  'ec2:tgw': sync_transit_gateways,
@@ -0,0 +1,43 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ import neo4j
5
+
6
+ from cartography.config import Config
7
+ from cartography.intel.entra.users import sync_entra_users
8
+ from cartography.util import timeit
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @timeit
14
+ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
15
+ """
16
+ If this module is configured, perform ingestion of Entra data. Otherwise warn and exit
17
+ :param neo4j_session: Neo4J session for database interface
18
+ :param config: A cartography.config object
19
+ :return: None
20
+ """
21
+
22
+ if not config.entra_tenant_id or not config.entra_client_id or not config.entra_client_secret:
23
+ logger.info(
24
+ 'Entra import is not configured - skipping this module. '
25
+ 'See docs to configure.',
26
+ )
27
+ return
28
+
29
+ common_job_parameters = {
30
+ "UPDATE_TAG": config.update_tag,
31
+ "TENANT_ID": config.entra_tenant_id,
32
+ }
33
+
34
+ asyncio.run(
35
+ sync_entra_users(
36
+ neo4j_session,
37
+ config.entra_tenant_id,
38
+ config.entra_client_id,
39
+ config.entra_client_secret,
40
+ config.update_tag,
41
+ common_job_parameters,
42
+ ),
43
+ )
@@ -0,0 +1,205 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ from azure.identity import ClientSecretCredential
6
+ from msgraph import GraphServiceClient
7
+ from msgraph.generated.models.organization import Organization
8
+ from msgraph.generated.models.user import User
9
+
10
+ from cartography.client.core.tx import load
11
+ from cartography.graph.job import GraphJob
12
+ from cartography.models.entra.tenant import EntraTenantSchema
13
+ from cartography.models.entra.user import EntraUserSchema
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ async def get_tenant(client: GraphServiceClient) -> Organization:
21
+ """
22
+ Get tenant information from Microsoft Graph API
23
+ """
24
+ org = await client.organization.get()
25
+ return org.value[0] # Get the first (and typically only) tenant
26
+
27
+
28
+ @timeit
29
+ async def get_users(client: GraphServiceClient) -> list[User]:
30
+ """
31
+ Get all users from Microsoft Graph API with pagination support
32
+ """
33
+ all_users: list[User] = []
34
+ request_configuration = client.users.UsersRequestBuilderGetRequestConfiguration(
35
+ query_parameters=client.users.UsersRequestBuilderGetQueryParameters(
36
+ # Request more items per page to reduce number of API calls
37
+ top=999,
38
+ ),
39
+ )
40
+
41
+ page = await client.users.get(request_configuration=request_configuration)
42
+ while page:
43
+ all_users.extend(page.value)
44
+ if not page.odata_next_link:
45
+ break
46
+ page = await client.users.with_url(page.odata_next_link).get()
47
+
48
+ return all_users
49
+
50
+
51
+ @timeit
52
+ def transform_users(users: list[User]) -> list[dict[str, Any]]:
53
+ """
54
+ Transform the API response into the format expected by our schema
55
+ """
56
+ result: list[dict[str, Any]] = []
57
+ for user in users:
58
+ transformed_user = {
59
+ 'id': user.id,
60
+ 'user_principal_name': user.user_principal_name,
61
+ 'display_name': user.display_name,
62
+ 'given_name': user.given_name,
63
+ 'surname': user.surname,
64
+ 'mail': user.mail,
65
+ 'other_mails': user.other_mails,
66
+ 'preferred_language': user.preferred_language,
67
+ 'preferred_name': user.preferred_name,
68
+ 'state': user.state,
69
+ 'usage_location': user.usage_location,
70
+ 'user_type': user.user_type,
71
+ 'show_in_address_list': user.show_in_address_list,
72
+ 'sign_in_sessions_valid_from_date_time': user.sign_in_sessions_valid_from_date_time,
73
+ 'security_identifier': user.on_premises_security_identifier,
74
+ 'account_enabled': user.account_enabled,
75
+ 'age_group': user.age_group,
76
+ 'business_phones': user.business_phones,
77
+ 'city': user.city,
78
+ 'company_name': user.company_name,
79
+ 'consent_provided_for_minor': user.consent_provided_for_minor,
80
+ 'country': user.country,
81
+ 'created_date_time': user.created_date_time,
82
+ 'creation_type': user.creation_type,
83
+ 'deleted_date_time': user.deleted_date_time,
84
+ 'department': user.department,
85
+ 'employee_id': user.employee_id,
86
+ 'employee_type': user.employee_type,
87
+ 'external_user_state': user.external_user_state,
88
+ 'external_user_state_change_date_time': user.external_user_state_change_date_time,
89
+ 'hire_date': user.hire_date,
90
+ 'is_management_restricted': user.is_management_restricted,
91
+ 'is_resource_account': user.is_resource_account,
92
+ 'job_title': user.job_title,
93
+ 'last_password_change_date_time': user.last_password_change_date_time,
94
+ 'mail_nickname': user.mail_nickname,
95
+ 'office_location': user.office_location,
96
+ 'on_premises_distinguished_name': user.on_premises_distinguished_name,
97
+ 'on_premises_domain_name': user.on_premises_domain_name,
98
+ 'on_premises_immutable_id': user.on_premises_immutable_id,
99
+ 'on_premises_last_sync_date_time': user.on_premises_last_sync_date_time,
100
+ 'on_premises_sam_account_name': user.on_premises_sam_account_name,
101
+ 'on_premises_security_identifier': user.on_premises_security_identifier,
102
+ 'on_premises_sync_enabled': user.on_premises_sync_enabled,
103
+ 'on_premises_user_principal_name': user.on_premises_user_principal_name,
104
+ }
105
+ result.append(transformed_user)
106
+ return result
107
+
108
+
109
+ @timeit
110
+ def transform_tenant(tenant: Organization, tenant_id: str) -> dict[str, Any]:
111
+ """
112
+ Transform the tenant data into the format expected by our schema
113
+ """
114
+ return {
115
+ 'id': tenant_id,
116
+ 'created_date_time': tenant.created_date_time,
117
+ 'default_usage_location': tenant.default_usage_location,
118
+ 'deleted_date_time': tenant.deleted_date_time,
119
+ 'display_name': tenant.display_name,
120
+ 'marketing_notification_emails': tenant.marketing_notification_emails,
121
+ 'mobile_device_management_authority': tenant.mobile_device_management_authority,
122
+ 'on_premises_last_sync_date_time': tenant.on_premises_last_sync_date_time,
123
+ 'on_premises_sync_enabled': tenant.on_premises_sync_enabled,
124
+ 'partner_tenant_type': tenant.partner_tenant_type,
125
+ 'postal_code': tenant.postal_code,
126
+ 'preferred_language': tenant.preferred_language,
127
+ 'state': tenant.state,
128
+ 'street': tenant.street,
129
+ 'tenant_type': tenant.tenant_type,
130
+ }
131
+
132
+
133
+ @timeit
134
+ def load_tenant(
135
+ neo4j_session: neo4j.Session,
136
+ tenant: dict[str, Any],
137
+ update_tag: int,
138
+ ) -> None:
139
+ load(
140
+ neo4j_session,
141
+ EntraTenantSchema(),
142
+ [tenant],
143
+ lastupdated=update_tag,
144
+ )
145
+
146
+
147
+ @timeit
148
+ def load_users(
149
+ neo4j_session: neo4j.Session,
150
+ users: list[dict[str, Any]],
151
+ tenant_id: str,
152
+ update_tag: int,
153
+ ) -> None:
154
+ logger.info(f"Loading {len(users)} Entra users")
155
+ load(
156
+ neo4j_session,
157
+ EntraUserSchema(),
158
+ users,
159
+ lastupdated=update_tag,
160
+ TENANT_ID=tenant_id,
161
+ )
162
+
163
+
164
+ def cleanup(neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]) -> None:
165
+ GraphJob.from_node_schema(EntraUserSchema(), common_job_parameters).run(neo4j_session)
166
+
167
+
168
+ @timeit
169
+ async def sync_entra_users(
170
+ neo4j_session: neo4j.Session,
171
+ tenant_id: str,
172
+ client_id: str,
173
+ client_secret: str,
174
+ update_tag: int,
175
+ common_job_parameters: dict[str, Any],
176
+ ) -> None:
177
+ """
178
+ Sync Entra users and tenant information
179
+ :param neo4j_session: Neo4J session for database interface
180
+ :param tenant_id: Entra tenant ID
181
+ :param client_id: Entra application client ID
182
+ :param client_secret: Entra application client secret
183
+ :param update_tag: Timestamp used to determine data freshness
184
+ :param common_job_parameters: dict of other job parameters to carry to sub-jobs
185
+ :return: None
186
+ """
187
+ # Initialize Graph client
188
+ credential = ClientSecretCredential(
189
+ tenant_id=tenant_id,
190
+ client_id=client_id,
191
+ client_secret=client_secret,
192
+ )
193
+ client = GraphServiceClient(credential, scopes=['https://graph.microsoft.com/.default'])
194
+
195
+ # Get tenant information
196
+ tenant = await get_tenant(client)
197
+ users = await get_users(client)
198
+
199
+ transformed_users = transform_users(users)
200
+ transformed_tenant = transform_tenant(tenant, tenant_id)
201
+
202
+ load_tenant(neo4j_session, transformed_tenant, update_tag)
203
+ load_users(neo4j_session, transformed_users, tenant_id, update_tag)
204
+
205
+ cleanup(neo4j_session, common_job_parameters)
@@ -0,0 +1,68 @@
1
+ from dataclasses import dataclass
2
+
3
+ from cartography.models.core.common import PropertyRef
4
+ from cartography.models.core.nodes import CartographyNodeProperties
5
+ from cartography.models.core.nodes import CartographyNodeSchema
6
+ from cartography.models.core.nodes import ExtraNodeLabels
7
+ from cartography.models.core.relationships import CartographyRelProperties
8
+ from cartography.models.core.relationships import CartographyRelSchema
9
+ from cartography.models.core.relationships import LinkDirection
10
+ from cartography.models.core.relationships import make_target_node_matcher
11
+ from cartography.models.core.relationships import OtherRelationships
12
+ from cartography.models.core.relationships import TargetNodeMatcher
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ELBListenerNodeProperties(CartographyNodeProperties):
17
+ id: PropertyRef = PropertyRef('id')
18
+ port: PropertyRef = PropertyRef('port')
19
+ protocol: PropertyRef = PropertyRef('protocol')
20
+ instance_port: PropertyRef = PropertyRef('instance_port')
21
+ instance_protocol: PropertyRef = PropertyRef('instance_protocol')
22
+ policy_names: PropertyRef = PropertyRef('policy_names')
23
+ lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ELBListenerToLoadBalancerRelProperties(CartographyRelProperties):
28
+ lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ELBListenerToLoadBalancer(CartographyRelSchema):
33
+ target_node_label: str = 'LoadBalancer'
34
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
35
+ {'id': PropertyRef('LoadBalancerId')},
36
+ )
37
+ direction: LinkDirection = LinkDirection.INWARD
38
+ rel_label: str = "ELB_LISTENER"
39
+ properties: ELBListenerToLoadBalancerRelProperties = ELBListenerToLoadBalancerRelProperties()
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ELBListenerToAWSAccountRelProperties(CartographyRelProperties):
44
+ lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ELBListenerToAWSAccount(CartographyRelSchema):
49
+ target_node_label: str = 'AWSAccount'
50
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
51
+ {'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
52
+ )
53
+ direction: LinkDirection = LinkDirection.INWARD
54
+ rel_label: str = "RESOURCE"
55
+ properties: ELBListenerToAWSAccountRelProperties = ELBListenerToAWSAccountRelProperties()
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ELBListenerSchema(CartographyNodeSchema):
60
+ label: str = 'ELBListener'
61
+ properties: ELBListenerNodeProperties = ELBListenerNodeProperties()
62
+ extra_node_labels: ExtraNodeLabels = ExtraNodeLabels(['Endpoint'])
63
+ sub_resource_relationship: ELBListenerToAWSAccount = ELBListenerToAWSAccount()
64
+ other_relationships: OtherRelationships = OtherRelationships(
65
+ [
66
+ ELBListenerToLoadBalancer(),
67
+ ],
68
+ )