cartography 0.101.1rc1__py3-none-any.whl → 0.102.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.
- cartography/_version.py +2 -2
- cartography/cli.py +61 -0
- cartography/config.py +16 -0
- cartography/data/indexes.cypher +0 -6
- cartography/data/jobs/cleanup/crowdstrike_import_cleanup.json +0 -5
- cartography/intel/aws/__init__.py +11 -1
- cartography/intel/aws/ec2/launch_templates.py +14 -5
- 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/aws/util/common.py +27 -0
- cartography/intel/crowdstrike/__init__.py +17 -5
- cartography/intel/crowdstrike/endpoints.py +12 -44
- cartography/intel/entra/__init__.py +43 -0
- cartography/intel/entra/users.py +205 -0
- cartography/intel/kandji/devices.py +27 -3
- 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/crowdstrike/__init__.py +0 -0
- cartography/models/crowdstrike/hosts.py +49 -0
- cartography/models/entra/__init__.py +0 -0
- cartography/models/entra/tenant.py +33 -0
- cartography/models/entra/user.py +83 -0
- cartography/stats.py +1 -1
- cartography/sync.py +2 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/METADATA +4 -1
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/RECORD +34 -21
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/WHEEL +1 -1
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.101.1rc1.dist-info → cartography-0.102.0.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,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from typing import List
|
|
2
3
|
|
|
3
4
|
from cartography.intel.aws.resources import RESOURCE_FUNCTIONS
|
|
4
5
|
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
def parse_and_validate_aws_requested_syncs(aws_requested_syncs: str) -> List[str]:
|
|
7
10
|
validated_resources: List[str] = []
|
|
@@ -19,3 +22,27 @@ def parse_and_validate_aws_requested_syncs(aws_requested_syncs: str) -> List[str
|
|
|
19
22
|
f'Our full list of valid values is: {valid_syncs}.',
|
|
20
23
|
)
|
|
21
24
|
return validated_resources
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_and_validate_aws_regions(aws_regions: str) -> list[str]:
|
|
28
|
+
"""
|
|
29
|
+
Parse and validate a comma-separated string of AWS regions.
|
|
30
|
+
:param aws_regions: Comma-separated string of AWS regions
|
|
31
|
+
:return: A validated list of AWS regions
|
|
32
|
+
"""
|
|
33
|
+
validated_regions: List[str] = []
|
|
34
|
+
for region in aws_regions.split(','):
|
|
35
|
+
region = region.strip()
|
|
36
|
+
if region:
|
|
37
|
+
validated_regions.append(region)
|
|
38
|
+
else:
|
|
39
|
+
logger.warning(
|
|
40
|
+
f'Unable to parse string "{region}". Please check the value you passed to `aws-regions`. '
|
|
41
|
+
f'You specified "{aws_regions}". Continuing on with sync.',
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if not validated_regions:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f'`aws-regions` was set but no regions were specified. You provided this string: "{aws_regions}"',
|
|
47
|
+
)
|
|
48
|
+
return validated_regions
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
import neo4j
|
|
4
5
|
|
|
5
6
|
from cartography.config import Config
|
|
7
|
+
from cartography.graph.job import GraphJob
|
|
6
8
|
from cartography.intel.crowdstrike.endpoints import sync_hosts
|
|
7
9
|
from cartography.intel.crowdstrike.spotlight import sync_vulnerabilities
|
|
8
10
|
from cartography.intel.crowdstrike.util import get_authorization
|
|
11
|
+
from cartography.models.crowdstrike.hosts import CrowdstrikeHostSchema
|
|
9
12
|
from cartography.stats import get_stats_client
|
|
10
13
|
from cartography.util import merge_module_sync_metadata
|
|
11
14
|
from cartography.util import run_cleanup_job
|
|
@@ -50,11 +53,7 @@ def start_crowdstrike_ingestion(
|
|
|
50
53
|
config.update_tag,
|
|
51
54
|
authorization,
|
|
52
55
|
)
|
|
53
|
-
|
|
54
|
-
"crowdstrike_import_cleanup.json",
|
|
55
|
-
neo4j_session,
|
|
56
|
-
common_job_parameters,
|
|
57
|
-
)
|
|
56
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
58
57
|
|
|
59
58
|
group_id = "public"
|
|
60
59
|
if config.crowdstrike_api_url:
|
|
@@ -67,3 +66,16 @@ def start_crowdstrike_ingestion(
|
|
|
67
66
|
update_tag=config.update_tag,
|
|
68
67
|
stat_handler=stat_handler,
|
|
69
68
|
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@timeit
|
|
72
|
+
def cleanup(neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]) -> None:
|
|
73
|
+
logger.info("Running Crowdstrike cleanup")
|
|
74
|
+
GraphJob.from_node_schema(CrowdstrikeHostSchema(), common_job_parameters).run(neo4j_session)
|
|
75
|
+
|
|
76
|
+
# Cleanup other crowdstrike assets not handled by the data model
|
|
77
|
+
run_cleanup_job(
|
|
78
|
+
"crowdstrike_import_cleanup.json",
|
|
79
|
+
neo4j_session,
|
|
80
|
+
common_job_parameters,
|
|
81
|
+
)
|
|
@@ -6,6 +6,8 @@ import neo4j
|
|
|
6
6
|
from falconpy.hosts import Hosts
|
|
7
7
|
from falconpy.oauth2 import OAuth2
|
|
8
8
|
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.models.crowdstrike.hosts import CrowdstrikeHostSchema
|
|
9
11
|
from cartography.util import timeit
|
|
10
12
|
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
@@ -24,55 +26,21 @@ def sync_hosts(
|
|
|
24
26
|
load_host_data(neo4j_session, host_data, update_tag)
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
@timeit
|
|
27
30
|
def load_host_data(
|
|
28
|
-
neo4j_session: neo4j.Session,
|
|
31
|
+
neo4j_session: neo4j.Session,
|
|
32
|
+
data: List[Dict],
|
|
33
|
+
update_tag: int,
|
|
29
34
|
) -> None:
|
|
30
35
|
"""
|
|
31
|
-
|
|
32
|
-
"""
|
|
33
|
-
ingestion_cypher_query = """
|
|
34
|
-
UNWIND $Hosts AS host
|
|
35
|
-
MERGE (h:CrowdstrikeHost{id: host.device_id})
|
|
36
|
-
ON CREATE SET h.cid = host.cid,
|
|
37
|
-
h.instance_id = host.instance_id,
|
|
38
|
-
h.serial_number = host.serial_number,
|
|
39
|
-
h.firstseen = timestamp()
|
|
40
|
-
SET h.status = host.status,
|
|
41
|
-
h.hostname = host.hostname,
|
|
42
|
-
h.machine_domain = host.machine_domain,
|
|
43
|
-
h.crowdstrike_first_seen = host.first_seen,
|
|
44
|
-
h.crowdstrike_last_seen = host.last_seen,
|
|
45
|
-
h.local_ip = host.local_ip,
|
|
46
|
-
h.external_ip = host.external_ip,
|
|
47
|
-
h.cpu_signature = host.cpu_signature,
|
|
48
|
-
h.bios_manufacturer = host.bios_manufacturer,
|
|
49
|
-
h.bios_version = host.bios_version,
|
|
50
|
-
h.mac_address = host.mac_address,
|
|
51
|
-
h.os_version = host.os_version,
|
|
52
|
-
h.os_build = host.os_build,
|
|
53
|
-
h.platform_id = host.platform_id,
|
|
54
|
-
h.platform_name = host.platform_name,
|
|
55
|
-
h.service_provider = host.service_provider,
|
|
56
|
-
h.service_provider_account_id = host.service_provider_account_id,
|
|
57
|
-
h.agent_version = host.agent_version,
|
|
58
|
-
h.system_manufacturer = host.system_manufacturer,
|
|
59
|
-
h.system_product_name = host.system_product_name,
|
|
60
|
-
h.product_type = host.product_type,
|
|
61
|
-
h.product_type_desc = host.product_type_desc,
|
|
62
|
-
h.provision_status = host.provision_status,
|
|
63
|
-
h.reduced_functionality_mode = host.reduced_functionality_mode,
|
|
64
|
-
h.kernel_version = host.kernel_version,
|
|
65
|
-
h.major_version = host.major_version,
|
|
66
|
-
h.minor_version = host.minor_version,
|
|
67
|
-
h.tags = host.tags,
|
|
68
|
-
h.modified_timestamp = host.modified_timestamp,
|
|
69
|
-
h.lastupdated = $update_tag
|
|
36
|
+
Load Crowdstrike host data into Neo4j.
|
|
70
37
|
"""
|
|
71
38
|
logger.info(f"Loading {len(data)} crowdstrike hosts.")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
39
|
+
load(
|
|
40
|
+
neo4j_session,
|
|
41
|
+
CrowdstrikeHostSchema(),
|
|
42
|
+
data,
|
|
43
|
+
lastupdated=update_tag,
|
|
76
44
|
)
|
|
77
45
|
|
|
78
46
|
|
|
@@ -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
|
+
)
|