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.
- cartography/_version.py +2 -2
- cartography/cli.py +38 -0
- cartography/config.py +12 -0
- cartography/data/indexes.cypher +0 -3
- cartography/intel/aws/ec2/launch_templates.py +27 -32
- cartography/intel/aws/ec2/load_balancers.py +126 -148
- cartography/intel/aws/ec2/route_tables.py +287 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/entra/__init__.py +43 -0
- cartography/intel/entra/users.py +205 -0
- cartography/models/aws/ec2/load_balancer_listeners.py +68 -0
- cartography/models/aws/ec2/load_balancers.py +102 -0
- cartography/models/aws/ec2/route_table_associations.py +87 -0
- cartography/models/aws/ec2/route_tables.py +121 -0
- cartography/models/aws/ec2/routes.py +77 -0
- cartography/models/entra/__init__.py +0 -0
- cartography/models/entra/tenant.py +33 -0
- cartography/models/entra/user.py +83 -0
- cartography/sync.py +2 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc2.dist-info}/METADATA +4 -1
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc2.dist-info}/RECORD +25 -14
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc2.dist-info}/WHEEL +0 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc2.dist-info}/entry_points.txt +0 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc2.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.101.1rc2.dist-info → cartography-0.102.0rc2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|