cartography 0.114.0__py3-none-any.whl → 0.115.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 +2 -2
- cartography/client/core/tx.py +11 -0
- cartography/intel/aws/config.py +7 -3
- cartography/intel/aws/ecr.py +9 -9
- cartography/intel/aws/identitycenter.py +240 -13
- cartography/intel/aws/lambda_function.py +69 -2
- cartography/intel/aws/organizations.py +3 -1
- cartography/intel/aws/permission_relationships.py +3 -1
- cartography/intel/aws/redshift.py +9 -4
- cartography/intel/aws/route53.py +53 -3
- cartography/intel/aws/securityhub.py +3 -1
- cartography/intel/azure/__init__.py +8 -0
- cartography/intel/azure/logic_apps.py +101 -0
- cartography/intel/create_indexes.py +2 -1
- cartography/intel/dns.py +5 -2
- cartography/intel/gcp/dns.py +2 -1
- cartography/intel/github/repos.py +3 -6
- cartography/intel/gsuite/api.py +17 -4
- cartography/intel/okta/applications.py +9 -4
- cartography/intel/okta/awssaml.py +5 -2
- cartography/intel/okta/factors.py +3 -1
- cartography/intel/okta/groups.py +5 -2
- cartography/intel/okta/organization.py +3 -1
- cartography/intel/okta/origins.py +3 -1
- cartography/intel/okta/roles.py +5 -2
- cartography/intel/okta/users.py +3 -1
- cartography/models/aws/identitycenter/awspermissionset.py +24 -1
- cartography/models/aws/identitycenter/awssogroup.py +70 -0
- cartography/models/aws/identitycenter/awsssouser.py +37 -1
- cartography/models/aws/lambda_function/lambda_function.py +2 -0
- cartography/models/azure/logic_apps.py +56 -0
- cartography/models/entra/user.py +18 -0
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/METADATA +3 -2
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/RECORD +39 -36
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/WHEEL +0 -0
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.114.0.dist-info → cartography-0.115.0.dist-info}/top_level.txt +0 -0
cartography/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.115.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 115, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
cartography/cli.py
CHANGED
|
@@ -967,8 +967,8 @@ class CLI:
|
|
|
967
967
|
logger.warning("A Kandji base URI was provided but a token was not.")
|
|
968
968
|
config.kandji_token = None
|
|
969
969
|
else:
|
|
970
|
-
logger.warning("A Kandji base URI was not provided.")
|
|
971
970
|
config.kandji_base_uri = None
|
|
971
|
+
config.kandji_token = None
|
|
972
972
|
|
|
973
973
|
if config.statsd_enabled:
|
|
974
974
|
logger.debug(
|
|
@@ -1096,8 +1096,8 @@ class CLI:
|
|
|
1096
1096
|
logger.warning("A SnipeIT base URI was provided but a token was not.")
|
|
1097
1097
|
config.snipeit_token = None
|
|
1098
1098
|
else:
|
|
1099
|
-
logger.warning("A SnipeIT base URI was not provided.")
|
|
1100
1099
|
config.snipeit_base_uri = None
|
|
1100
|
+
config.snipeit_token = None
|
|
1101
1101
|
|
|
1102
1102
|
# Tailscale config
|
|
1103
1103
|
if config.tailscale_token_env_var:
|
cartography/client/core/tx.py
CHANGED
|
@@ -19,6 +19,17 @@ from cartography.util import batch
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def run_write_query(
|
|
23
|
+
neo4j_session: neo4j.Session, query: str, **parameters: Any
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Execute a write query inside a managed transaction."""
|
|
26
|
+
|
|
27
|
+
def _run_query_tx(tx: neo4j.Transaction) -> None:
|
|
28
|
+
tx.run(query, **parameters).consume()
|
|
29
|
+
|
|
30
|
+
neo4j_session.execute_write(_run_query_tx)
|
|
31
|
+
|
|
32
|
+
|
|
22
33
|
def read_list_of_values_tx(
|
|
23
34
|
tx: neo4j.Transaction,
|
|
24
35
|
query: str,
|
cartography/intel/aws/config.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import List
|
|
|
5
5
|
import boto3
|
|
6
6
|
import neo4j
|
|
7
7
|
|
|
8
|
+
from cartography.client.core.tx import run_write_query
|
|
8
9
|
from cartography.util import aws_handle_regions
|
|
9
10
|
from cartography.util import run_cleanup_job
|
|
10
11
|
from cartography.util import timeit
|
|
@@ -80,7 +81,8 @@ def load_configuration_recorders(
|
|
|
80
81
|
for recorder in data:
|
|
81
82
|
recorder["_id"] = f'{recorder["name"]}:{current_aws_account_id}:{region}'
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
run_write_query(
|
|
85
|
+
neo4j_session,
|
|
84
86
|
ingest_configuration_recorders,
|
|
85
87
|
Recorders=data,
|
|
86
88
|
Region=region,
|
|
@@ -120,7 +122,8 @@ def load_delivery_channels(
|
|
|
120
122
|
for channel in data:
|
|
121
123
|
channel["_id"] = f'{channel["name"]}:{current_aws_account_id}:{region}'
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
run_write_query(
|
|
126
|
+
neo4j_session,
|
|
124
127
|
ingest_delivery_channels,
|
|
125
128
|
Channels=data,
|
|
126
129
|
Region=region,
|
|
@@ -167,7 +170,8 @@ def load_config_rules(
|
|
|
167
170
|
for detail in rule["Source"]["SourceDetails"]:
|
|
168
171
|
details.append(f"{detail}")
|
|
169
172
|
rule["_source_details"] = details
|
|
170
|
-
|
|
173
|
+
run_write_query(
|
|
174
|
+
neo4j_session,
|
|
171
175
|
ingest_config_rules,
|
|
172
176
|
Rules=data,
|
|
173
177
|
Region=region,
|
cartography/intel/aws/ecr.py
CHANGED
|
@@ -57,15 +57,15 @@ def get_ecr_repository_images(
|
|
|
57
57
|
)
|
|
58
58
|
for response in describe_response:
|
|
59
59
|
image_details = response["imageDetails"]
|
|
60
|
-
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
for detail in image_details:
|
|
61
|
+
tags = detail.get("imageTags") or []
|
|
62
|
+
if tags:
|
|
63
|
+
for tag in tags:
|
|
64
|
+
image_detail = {**detail, "imageTag": tag}
|
|
65
|
+
image_detail.pop("imageTags", None)
|
|
66
|
+
ecr_repository_images.append(image_detail)
|
|
67
|
+
else:
|
|
68
|
+
ecr_repository_images.append({**detail})
|
|
69
69
|
return ecr_repository_images
|
|
70
70
|
|
|
71
71
|
|
|
@@ -2,6 +2,8 @@ import logging
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
from typing import Dict
|
|
4
4
|
from typing import List
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from typing import Union
|
|
5
7
|
|
|
6
8
|
import boto3
|
|
7
9
|
import neo4j
|
|
@@ -15,9 +17,13 @@ from cartography.models.aws.identitycenter.awsidentitycenter import (
|
|
|
15
17
|
from cartography.models.aws.identitycenter.awspermissionset import (
|
|
16
18
|
AWSPermissionSetSchema,
|
|
17
19
|
)
|
|
20
|
+
from cartography.models.aws.identitycenter.awspermissionset import (
|
|
21
|
+
RoleAssignmentAllowedByGroupMatchLink,
|
|
22
|
+
)
|
|
18
23
|
from cartography.models.aws.identitycenter.awspermissionset import (
|
|
19
24
|
RoleAssignmentAllowedByMatchLink,
|
|
20
25
|
)
|
|
26
|
+
from cartography.models.aws.identitycenter.awssogroup import AWSSSOGroupSchema
|
|
21
27
|
from cartography.models.aws.identitycenter.awsssouser import AWSSSOUserSchema
|
|
22
28
|
from cartography.util import aws_handle_regions
|
|
23
29
|
from cartography.util import timeit
|
|
@@ -150,18 +156,72 @@ def get_sso_users(
|
|
|
150
156
|
return users
|
|
151
157
|
|
|
152
158
|
|
|
153
|
-
|
|
159
|
+
@timeit
|
|
160
|
+
@aws_handle_regions
|
|
161
|
+
def get_sso_groups(
|
|
162
|
+
boto3_session: boto3.session.Session,
|
|
163
|
+
identity_store_id: str,
|
|
164
|
+
region: str,
|
|
165
|
+
) -> List[Dict]:
|
|
166
|
+
"""
|
|
167
|
+
Get all SSO groups for a given Identity Store
|
|
168
|
+
"""
|
|
169
|
+
client = boto3_session.client("identitystore", region_name=region)
|
|
170
|
+
groups: List[Dict[str, Any]] = []
|
|
171
|
+
|
|
172
|
+
paginator = client.get_paginator("list_groups")
|
|
173
|
+
for page in paginator.paginate(IdentityStoreId=identity_store_id):
|
|
174
|
+
group_page = page.get("Groups", [])
|
|
175
|
+
for group in group_page:
|
|
176
|
+
groups.append(group)
|
|
177
|
+
|
|
178
|
+
return groups
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def transform_sso_users(
|
|
182
|
+
users: List[Dict[str, Any]],
|
|
183
|
+
user_group_memberships: Optional[Dict[str, List[str]]] = None,
|
|
184
|
+
user_permission_sets: Optional[Dict[str, List[str]]] = None,
|
|
185
|
+
) -> List[Dict[str, Any]]:
|
|
154
186
|
"""
|
|
155
|
-
Transform SSO users to match the expected schema
|
|
187
|
+
Transform SSO users to match the expected schema, optionally including group memberships
|
|
156
188
|
"""
|
|
157
189
|
transformed_users = []
|
|
158
190
|
for user in users:
|
|
159
|
-
if user.get("ExternalIds")
|
|
191
|
+
if user.get("ExternalIds"):
|
|
160
192
|
user["ExternalId"] = user["ExternalIds"][0].get("Id")
|
|
193
|
+
# Add group memberships if provided
|
|
194
|
+
if user_group_memberships:
|
|
195
|
+
user["MemberOfGroups"] = user_group_memberships.get(user["UserId"], [])
|
|
196
|
+
# Add direct permission set assignments if provided
|
|
197
|
+
if user_permission_sets:
|
|
198
|
+
user["AssignedPermissionSets"] = user_permission_sets.get(
|
|
199
|
+
user["UserId"], []
|
|
200
|
+
)
|
|
161
201
|
transformed_users.append(user)
|
|
162
202
|
return transformed_users
|
|
163
203
|
|
|
164
204
|
|
|
205
|
+
def transform_sso_groups(
|
|
206
|
+
groups: List[Dict[str, Any]],
|
|
207
|
+
group_permission_sets: Optional[Dict[str, List[str]]] = None,
|
|
208
|
+
) -> List[Dict[str, Any]]:
|
|
209
|
+
"""
|
|
210
|
+
Transform SSO groups to match the expected schema, optionally including permission set assignments
|
|
211
|
+
"""
|
|
212
|
+
transformed_groups: List[Dict[str, Any]] = []
|
|
213
|
+
for group in groups:
|
|
214
|
+
if group.get("ExternalIds"):
|
|
215
|
+
group["ExternalId"] = group["ExternalIds"][0].get("Id")
|
|
216
|
+
# Add permission set assignments if provided
|
|
217
|
+
if group_permission_sets:
|
|
218
|
+
group["AssignedPermissionSets"] = group_permission_sets.get(
|
|
219
|
+
group["GroupId"], []
|
|
220
|
+
)
|
|
221
|
+
transformed_groups.append(group)
|
|
222
|
+
return transformed_groups
|
|
223
|
+
|
|
224
|
+
|
|
165
225
|
@timeit
|
|
166
226
|
def load_sso_users(
|
|
167
227
|
neo4j_session: neo4j.Session,
|
|
@@ -189,6 +249,33 @@ def load_sso_users(
|
|
|
189
249
|
)
|
|
190
250
|
|
|
191
251
|
|
|
252
|
+
@timeit
|
|
253
|
+
def load_sso_groups(
|
|
254
|
+
neo4j_session: neo4j.Session,
|
|
255
|
+
groups: List[Dict],
|
|
256
|
+
identity_store_id: str,
|
|
257
|
+
region: str,
|
|
258
|
+
aws_account_id: str,
|
|
259
|
+
aws_update_tag: int,
|
|
260
|
+
) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Load SSO groups into the graph
|
|
263
|
+
"""
|
|
264
|
+
logger.info(
|
|
265
|
+
f"Loading {len(groups)} SSO groups for identity store {identity_store_id} in region {region}",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
load(
|
|
269
|
+
neo4j_session,
|
|
270
|
+
AWSSSOGroupSchema(),
|
|
271
|
+
groups,
|
|
272
|
+
lastupdated=aws_update_tag,
|
|
273
|
+
IdentityStoreId=identity_store_id,
|
|
274
|
+
AWS_ID=aws_account_id,
|
|
275
|
+
Region=region,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
192
279
|
@timeit
|
|
193
280
|
@aws_handle_regions
|
|
194
281
|
def get_role_assignments(
|
|
@@ -225,6 +312,71 @@ def get_role_assignments(
|
|
|
225
312
|
return role_assignments
|
|
226
313
|
|
|
227
314
|
|
|
315
|
+
@timeit
|
|
316
|
+
@aws_handle_regions
|
|
317
|
+
def get_group_role_assignments(
|
|
318
|
+
boto3_session: boto3.session.Session,
|
|
319
|
+
groups: List[Dict],
|
|
320
|
+
instance_arn: str,
|
|
321
|
+
region: str,
|
|
322
|
+
) -> List[Dict]:
|
|
323
|
+
"""
|
|
324
|
+
Get role assignments for SSO groups
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
logger.info(f"Getting role assignments for {len(groups)} groups")
|
|
328
|
+
client = boto3_session.client("sso-admin", region_name=region)
|
|
329
|
+
role_assignments: List[Dict[str, Any]] = []
|
|
330
|
+
|
|
331
|
+
for group in groups:
|
|
332
|
+
group_id = group["GroupId"]
|
|
333
|
+
paginator = client.get_paginator("list_account_assignments_for_principal")
|
|
334
|
+
for page in paginator.paginate(
|
|
335
|
+
InstanceArn=instance_arn,
|
|
336
|
+
PrincipalId=group_id,
|
|
337
|
+
PrincipalType="GROUP",
|
|
338
|
+
):
|
|
339
|
+
for assignment in page.get("AccountAssignments", []):
|
|
340
|
+
role_assignments.append(
|
|
341
|
+
{
|
|
342
|
+
"GroupId": group_id,
|
|
343
|
+
"PermissionSetArn": assignment.get("PermissionSetArn"),
|
|
344
|
+
"AccountId": assignment.get("AccountId"),
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return role_assignments
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@timeit
|
|
352
|
+
@aws_handle_regions
|
|
353
|
+
def get_user_group_memberships(
|
|
354
|
+
boto3_session: boto3.session.Session,
|
|
355
|
+
identity_store_id: str,
|
|
356
|
+
groups: List[Dict],
|
|
357
|
+
region: str,
|
|
358
|
+
) -> Dict[str, List[str]]:
|
|
359
|
+
"""
|
|
360
|
+
Return a mapping of UserId -> [GroupIds] for all group memberships in the identity store.
|
|
361
|
+
"""
|
|
362
|
+
client = boto3_session.client("identitystore", region_name=region)
|
|
363
|
+
user_groups: Dict[str, List[str]] = {}
|
|
364
|
+
|
|
365
|
+
for group in groups:
|
|
366
|
+
group_id = group["GroupId"]
|
|
367
|
+
paginator = client.get_paginator("list_group_memberships")
|
|
368
|
+
for page in paginator.paginate(
|
|
369
|
+
IdentityStoreId=identity_store_id, GroupId=group_id
|
|
370
|
+
):
|
|
371
|
+
for membership in page.get("GroupMemberships", []):
|
|
372
|
+
member = membership.get("MemberId", {})
|
|
373
|
+
user_id = member.get("UserId")
|
|
374
|
+
if user_id:
|
|
375
|
+
user_groups.setdefault(user_id, []).append(group_id)
|
|
376
|
+
|
|
377
|
+
return user_groups
|
|
378
|
+
|
|
379
|
+
|
|
228
380
|
@timeit
|
|
229
381
|
def get_permset_roles(
|
|
230
382
|
neo4j_session: neo4j.Session,
|
|
@@ -270,14 +422,18 @@ def load_role_assignments(
|
|
|
270
422
|
role_assignments: List[Dict],
|
|
271
423
|
aws_account_id: str,
|
|
272
424
|
aws_update_tag: int,
|
|
425
|
+
matchlink_schema: Union[
|
|
426
|
+
RoleAssignmentAllowedByMatchLink,
|
|
427
|
+
RoleAssignmentAllowedByGroupMatchLink,
|
|
428
|
+
],
|
|
273
429
|
) -> None:
|
|
274
430
|
"""
|
|
275
|
-
Load role assignments into the graph using MatchLink schema
|
|
431
|
+
Load role assignments into the graph using the provided MatchLink schema
|
|
276
432
|
"""
|
|
277
433
|
logger.info(f"Loading {len(role_assignments)} role assignments")
|
|
278
434
|
load_matchlinks(
|
|
279
435
|
neo4j_session,
|
|
280
|
-
|
|
436
|
+
matchlink_schema,
|
|
281
437
|
role_assignments,
|
|
282
438
|
lastupdated=aws_update_tag,
|
|
283
439
|
_sub_resource_label="AWSAccount",
|
|
@@ -300,6 +456,9 @@ def cleanup(
|
|
|
300
456
|
GraphJob.from_node_schema(AWSSSOUserSchema(), common_job_parameters).run(
|
|
301
457
|
neo4j_session,
|
|
302
458
|
)
|
|
459
|
+
GraphJob.from_node_schema(AWSSSOGroupSchema(), common_job_parameters).run(
|
|
460
|
+
neo4j_session,
|
|
461
|
+
)
|
|
303
462
|
|
|
304
463
|
# Clean up role assignment MatchLinks
|
|
305
464
|
GraphJob.from_matchlink(
|
|
@@ -308,6 +467,12 @@ def cleanup(
|
|
|
308
467
|
common_job_parameters["AWS_ID"],
|
|
309
468
|
common_job_parameters["UPDATE_TAG"],
|
|
310
469
|
).run(neo4j_session)
|
|
470
|
+
GraphJob.from_matchlink(
|
|
471
|
+
RoleAssignmentAllowedByGroupMatchLink(),
|
|
472
|
+
"AWSAccount",
|
|
473
|
+
common_job_parameters["AWS_ID"],
|
|
474
|
+
common_job_parameters["UPDATE_TAG"],
|
|
475
|
+
).run(neo4j_session)
|
|
311
476
|
|
|
312
477
|
|
|
313
478
|
@timeit
|
|
@@ -350,35 +515,97 @@ def sync_identity_center_instances(
|
|
|
350
515
|
update_tag,
|
|
351
516
|
)
|
|
352
517
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
518
|
+
# Fetch groups first to avoid interleaving between groups and users
|
|
519
|
+
groups = get_sso_groups(boto3_session, identity_store_id, region)
|
|
520
|
+
|
|
521
|
+
# Get permission set assignments for groups
|
|
522
|
+
group_permission_sets: Dict[str, List[str]] = {}
|
|
523
|
+
group_role_assignments_raw = get_group_role_assignments(
|
|
524
|
+
boto3_session,
|
|
525
|
+
groups,
|
|
526
|
+
instance_arn,
|
|
527
|
+
region,
|
|
528
|
+
)
|
|
529
|
+
for assignment in group_role_assignments_raw:
|
|
530
|
+
group_id = assignment["GroupId"]
|
|
531
|
+
perm_set = assignment["PermissionSetArn"]
|
|
532
|
+
group_permission_sets.setdefault(group_id, []).append(perm_set)
|
|
533
|
+
|
|
534
|
+
# Transform and load groups with their permission set assignments FIRST
|
|
535
|
+
# so that user->group membership edges can attach in the same run.
|
|
536
|
+
transformed_groups = transform_sso_groups(groups, group_permission_sets)
|
|
537
|
+
load_sso_groups(
|
|
356
538
|
neo4j_session,
|
|
357
|
-
|
|
539
|
+
transformed_groups,
|
|
358
540
|
identity_store_id,
|
|
359
541
|
region,
|
|
360
542
|
current_aws_account_id,
|
|
361
543
|
update_tag,
|
|
362
544
|
)
|
|
363
545
|
|
|
364
|
-
#
|
|
365
|
-
|
|
546
|
+
# Handle users AFTER groups exist
|
|
547
|
+
users = get_sso_users(boto3_session, identity_store_id, region)
|
|
548
|
+
user_group_memberships = get_user_group_memberships(
|
|
549
|
+
boto3_session,
|
|
550
|
+
identity_store_id,
|
|
551
|
+
groups,
|
|
552
|
+
region,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Get direct permission set assignments for users
|
|
556
|
+
user_permission_sets: Dict[str, List[str]] = {}
|
|
557
|
+
user_role_assignments_raw = get_role_assignments(
|
|
366
558
|
boto3_session,
|
|
367
559
|
users,
|
|
368
560
|
instance_arn,
|
|
369
561
|
region,
|
|
370
562
|
)
|
|
563
|
+
for assignment in user_role_assignments_raw:
|
|
564
|
+
uid = assignment["UserId"]
|
|
565
|
+
perm_set = assignment["PermissionSetArn"]
|
|
566
|
+
user_permission_sets.setdefault(uid, []).append(perm_set)
|
|
371
567
|
|
|
372
|
-
#
|
|
568
|
+
# Transform and load users with their group memberships AFTER groups exist
|
|
569
|
+
transformed_users = transform_sso_users(
|
|
570
|
+
users,
|
|
571
|
+
user_group_memberships,
|
|
572
|
+
user_permission_sets,
|
|
573
|
+
)
|
|
574
|
+
load_sso_users(
|
|
575
|
+
neo4j_session,
|
|
576
|
+
transformed_users,
|
|
577
|
+
identity_store_id,
|
|
578
|
+
region,
|
|
579
|
+
current_aws_account_id,
|
|
580
|
+
update_tag,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Enrich role assignments with exact role ARNs using permission set relationships.
|
|
584
|
+
# Note: we do this after groups and users are loaded so that
|
|
585
|
+
# load_role_assignments calls can MATCH existing AWSSSOUser/AWSSSOGroup
|
|
586
|
+
# nodes when drawing the ALLOWED_BY edges.
|
|
373
587
|
enriched_role_assignments = get_permset_roles(
|
|
374
588
|
neo4j_session,
|
|
375
|
-
|
|
589
|
+
user_role_assignments_raw,
|
|
376
590
|
)
|
|
377
591
|
load_role_assignments(
|
|
378
592
|
neo4j_session,
|
|
379
593
|
enriched_role_assignments,
|
|
380
594
|
current_aws_account_id,
|
|
381
595
|
update_tag,
|
|
596
|
+
RoleAssignmentAllowedByMatchLink(),
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
enriched_group_role_assignments = get_permset_roles(
|
|
600
|
+
neo4j_session,
|
|
601
|
+
group_role_assignments_raw,
|
|
602
|
+
)
|
|
603
|
+
load_role_assignments(
|
|
604
|
+
neo4j_session,
|
|
605
|
+
enriched_group_role_assignments,
|
|
606
|
+
current_aws_account_id,
|
|
607
|
+
update_tag,
|
|
608
|
+
RoleAssignmentAllowedByGroupMatchLink(),
|
|
382
609
|
)
|
|
383
610
|
|
|
384
611
|
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
from typing import Any
|
|
3
4
|
from typing import Dict
|
|
@@ -6,6 +7,7 @@ from typing import List
|
|
|
6
7
|
import boto3
|
|
7
8
|
import botocore
|
|
8
9
|
import neo4j
|
|
10
|
+
from policyuniverse.policy import Policy
|
|
9
11
|
|
|
10
12
|
from cartography.client.core.tx import load
|
|
11
13
|
from cartography.graph.job import GraphJob
|
|
@@ -36,7 +38,11 @@ def get_lambda_data(boto3_session: boto3.session.Session, region: str) -> List[D
|
|
|
36
38
|
return lambda_functions
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
def transform_lambda_functions(
|
|
41
|
+
def transform_lambda_functions(
|
|
42
|
+
lambda_functions: List[Dict],
|
|
43
|
+
permissions_by_arn: Dict[str, Dict[str, Any]],
|
|
44
|
+
region: str,
|
|
45
|
+
) -> List[Dict]:
|
|
40
46
|
transformed_functions = []
|
|
41
47
|
|
|
42
48
|
for function_data in lambda_functions:
|
|
@@ -48,6 +54,11 @@ def transform_lambda_functions(lambda_functions: List[Dict], region: str) -> Lis
|
|
|
48
54
|
|
|
49
55
|
transformed_function["Region"] = region
|
|
50
56
|
|
|
57
|
+
function_arn = function_data["FunctionArn"]
|
|
58
|
+
permission_data = permissions_by_arn[function_arn]
|
|
59
|
+
transformed_function["AnonymousAccess"] = permission_data["AnonymousAccess"]
|
|
60
|
+
transformed_function["AnonymousActions"] = permission_data["AnonymousActions"]
|
|
61
|
+
|
|
51
62
|
transformed_functions.append(transformed_function)
|
|
52
63
|
|
|
53
64
|
return transformed_functions
|
|
@@ -126,6 +137,61 @@ def get_event_source_mappings(
|
|
|
126
137
|
return event_source_mappings
|
|
127
138
|
|
|
128
139
|
|
|
140
|
+
@timeit
|
|
141
|
+
@aws_handle_regions
|
|
142
|
+
def get_lambda_permissions(
|
|
143
|
+
lambda_functions: List[Dict],
|
|
144
|
+
boto3_session: boto3.Session,
|
|
145
|
+
region: str,
|
|
146
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
147
|
+
"""
|
|
148
|
+
Get Lambda permissions for the given functions in the specified region.
|
|
149
|
+
"""
|
|
150
|
+
client = boto3_session.client("lambda", region_name=region)
|
|
151
|
+
all_permissions = {}
|
|
152
|
+
for function in lambda_functions:
|
|
153
|
+
function_name = function["FunctionName"]
|
|
154
|
+
function_arn = function["FunctionArn"]
|
|
155
|
+
|
|
156
|
+
all_permissions[function_arn] = {
|
|
157
|
+
"AnonymousAccess": None,
|
|
158
|
+
"AnonymousActions": None,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
response = client.get_policy(FunctionName=function_name)
|
|
163
|
+
policy = response.get("Policy")
|
|
164
|
+
|
|
165
|
+
if policy:
|
|
166
|
+
parsed_policy = parse_policy(function_arn, policy)
|
|
167
|
+
all_permissions[function_arn] = {
|
|
168
|
+
"AnonymousAccess": parsed_policy.get("AnonymousAccess"),
|
|
169
|
+
"AnonymousActions": parsed_policy.get("AnonymousActions"),
|
|
170
|
+
}
|
|
171
|
+
except client.exceptions.ResourceNotFoundException:
|
|
172
|
+
logger.debug(f"No policy found for Lambda function {function_name}")
|
|
173
|
+
pass
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.warning(
|
|
176
|
+
f"Error getting policy for Lambda function {function_name}: {e}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return all_permissions
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def parse_policy(function_arn: str, policy: str) -> Dict[str, Any]:
|
|
183
|
+
"""
|
|
184
|
+
Parse the Lambda permission policy to extract anonymous access and actions.
|
|
185
|
+
"""
|
|
186
|
+
policy_obj = Policy(json.loads(policy))
|
|
187
|
+
inet_actions = policy_obj.internet_accessible_actions()
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"AnonymousAccess": policy_obj.is_internet_accessible(),
|
|
191
|
+
"AnonymousActions": list(inet_actions) if inet_actions else [],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
129
195
|
@timeit
|
|
130
196
|
def load_lambda_function_aliases(
|
|
131
197
|
neo4j_session: neo4j.Session,
|
|
@@ -306,7 +372,8 @@ def sync(
|
|
|
306
372
|
|
|
307
373
|
# Get and load core lambda functions
|
|
308
374
|
data = get_lambda_data(boto3_session, region)
|
|
309
|
-
|
|
375
|
+
permissions_by_arn = get_lambda_permissions(data, boto3_session, region)
|
|
376
|
+
transformed_data = transform_lambda_functions(data, permissions_by_arn, region)
|
|
310
377
|
load_lambda_functions(
|
|
311
378
|
neo4j_session,
|
|
312
379
|
transformed_data,
|
|
@@ -5,6 +5,7 @@ import boto3
|
|
|
5
5
|
import botocore.exceptions
|
|
6
6
|
import neo4j
|
|
7
7
|
|
|
8
|
+
from cartography.client.core.tx import run_write_query
|
|
8
9
|
from cartography.intel.aws.iam import sync_root_principal
|
|
9
10
|
from cartography.util import timeit
|
|
10
11
|
|
|
@@ -114,7 +115,8 @@ def load_aws_accounts(
|
|
|
114
115
|
"""
|
|
115
116
|
for account_name, account_id in aws_accounts.items():
|
|
116
117
|
root_arn = f"arn:aws:iam::{account_id}:root"
|
|
117
|
-
|
|
118
|
+
run_write_query(
|
|
119
|
+
neo4j_session,
|
|
118
120
|
query,
|
|
119
121
|
ACCOUNT_ID=account_id,
|
|
120
122
|
ACCOUNT_NAME=account_name,
|
|
@@ -13,6 +13,7 @@ import neo4j
|
|
|
13
13
|
import yaml
|
|
14
14
|
|
|
15
15
|
from cartography.client.core.tx import read_list_of_dicts_tx
|
|
16
|
+
from cartography.client.core.tx import run_write_query
|
|
16
17
|
from cartography.graph.statement import GraphStatement
|
|
17
18
|
from cartography.util import timeit
|
|
18
19
|
|
|
@@ -329,7 +330,8 @@ def load_principal_mappings(
|
|
|
329
330
|
node_label=node_label,
|
|
330
331
|
relationship_name=relationship_name,
|
|
331
332
|
)
|
|
332
|
-
|
|
333
|
+
run_write_query(
|
|
334
|
+
neo4j_session,
|
|
333
335
|
map_policy_query_template,
|
|
334
336
|
Mapping=principal_mappings,
|
|
335
337
|
aws_update_tag=update_tag,
|
|
@@ -5,6 +5,7 @@ from typing import List
|
|
|
5
5
|
import boto3
|
|
6
6
|
import neo4j
|
|
7
7
|
|
|
8
|
+
from cartography.client.core.tx import run_write_query
|
|
8
9
|
from cartography.util import aws_handle_regions
|
|
9
10
|
from cartography.util import run_cleanup_job
|
|
10
11
|
from cartography.util import timeit
|
|
@@ -88,7 +89,8 @@ def load_redshift_cluster_data(
|
|
|
88
89
|
SET r.lastupdated = $aws_update_tag
|
|
89
90
|
"""
|
|
90
91
|
for cluster in clusters:
|
|
91
|
-
|
|
92
|
+
run_write_query(
|
|
93
|
+
neo4j_session,
|
|
92
94
|
ingest_cluster,
|
|
93
95
|
Arn=cluster["arn"],
|
|
94
96
|
AZ=cluster["AvailabilityZone"],
|
|
@@ -128,7 +130,8 @@ def _attach_ec2_security_groups(
|
|
|
128
130
|
SET m.lastupdated = $aws_update_tag
|
|
129
131
|
"""
|
|
130
132
|
for group in cluster.get("VpcSecurityGroups", []):
|
|
131
|
-
|
|
133
|
+
run_write_query(
|
|
134
|
+
neo4j_session,
|
|
132
135
|
attach_cluster_to_group,
|
|
133
136
|
ClusterArn=cluster["arn"],
|
|
134
137
|
GroupId=group["VpcSecurityGroupId"],
|
|
@@ -150,7 +153,8 @@ def _attach_iam_roles(
|
|
|
150
153
|
SET s.lastupdated = $aws_update_tag
|
|
151
154
|
"""
|
|
152
155
|
for role in cluster.get("IamRoles", []):
|
|
153
|
-
|
|
156
|
+
run_write_query(
|
|
157
|
+
neo4j_session,
|
|
154
158
|
attach_cluster_to_role,
|
|
155
159
|
ClusterArn=cluster["arn"],
|
|
156
160
|
RoleArn=role["IamRoleArn"],
|
|
@@ -172,7 +176,8 @@ def _attach_aws_vpc(
|
|
|
172
176
|
SET m.lastupdated = $aws_update_tag
|
|
173
177
|
"""
|
|
174
178
|
if cluster.get("VpcId"):
|
|
175
|
-
|
|
179
|
+
run_write_query(
|
|
180
|
+
neo4j_session,
|
|
176
181
|
attach_cluster_to_vpc,
|
|
177
182
|
ClusterArn=cluster["arn"],
|
|
178
183
|
VpcId=cluster["VpcId"],
|