Skip to content

Commit 42c7c44

Browse files
authored
Collect SCPs for access edges (#2235)
1 parent d989475 commit 42c7c44

File tree

4 files changed

+148
-8
lines changed

4 files changed

+148
-8
lines changed

plugins/aws/fix_plugin_aws/access_edges.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from functools import lru_cache
22
from attr import frozen, define
3-
from fix_plugin_aws.resource.base import AwsResource, GraphBuilder
3+
from fix_plugin_aws.resource.base import AwsAccount, AwsResource, GraphBuilder
44

55
from typing import List, Literal, Set, Optional, Tuple, Union, Pattern
66

@@ -632,6 +632,15 @@ def __init__(self, builder: GraphBuilder):
632632
self._init_principals()
633633

634634
def _init_principals(self) -> None:
635+
636+
account_id = self.builder.account.id
637+
service_control_policy_levels: List[List[PolicyDocument]] = []
638+
account = next(self.builder.nodes(clazz=AwsAccount, filter=lambda a: a.id == account_id), None)
639+
if account and account._service_control_policies:
640+
service_control_policy_levels = [
641+
[PolicyDocument(json) for json in level] for level in account._service_control_policies
642+
]
643+
635644
for node in self.builder.nodes(clazz=AwsResource):
636645
if isinstance(node, AwsIamUser):
637646

@@ -643,9 +652,6 @@ def _init_principals(self) -> None:
643652
if pdj := pb_policy.policy_document_json():
644653
permission_boundaries.append(PolicyDocument(pdj))
645654

646-
# todo: collect these resources
647-
service_control_policy_levels: List[List[PolicyDocument]] = []
648-
649655
request_context = IamRequestContext(
650656
principal=node,
651657
identity_policies=identity_based_policies,
@@ -657,8 +663,6 @@ def _init_principals(self) -> None:
657663

658664
if isinstance(node, AwsIamGroup):
659665
identity_based_policies = self._get_group_based_policies(node)
660-
# todo: collect these resources
661-
service_control_policy_levels = []
662666

663667
request_context = IamRequestContext(
664668
principal=node,
@@ -677,8 +681,6 @@ def _init_principals(self) -> None:
677681
for pb_policy in self.builder.nodes(clazz=AwsIamPolicy, filter=lambda p: p.arn == pb_arn):
678682
if pdj := pb_policy.policy_document_json():
679683
permission_boundaries.append(PolicyDocument(pdj))
680-
# todo: collect these resources
681-
service_control_policy_levels = []
682684

683685
request_context = IamRequestContext(
684686
principal=node,

plugins/aws/fix_plugin_aws/collector.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
waf,
4949
backup,
5050
bedrock,
51+
scp,
5152
)
5253
from fix_plugin_aws.resource.base import (
5354
AwsAccount,
@@ -469,3 +470,6 @@ def add_accounts(parent: Union[AwsOrganizationalRoot, AwsOrganizationalUnit]) ->
469470
create_org_graph()
470471
except Exception as e:
471472
log.exception(f"Error creating organization graph: {e}")
473+
474+
if account_scps := scp.collect_account_scps(self.account.id, self.config.scrape_org_role_arn, self.client):
475+
self.account._service_control_policies = account_scps

plugins/aws/fix_plugin_aws/resource/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ class AwsAccount(BaseAccount, AwsResource, BaseIamPrincipal):
310310
is_organization_master: bool = False
311311
organization_id: Optional[str] = None
312312
organization_arn: Optional[str] = None
313+
_service_control_policies: Optional[List[List[Json]]] = None
313314

314315

315316
default_ctime = datetime(2006, 3, 19, tzinfo=timezone.utc) # AWS public launch date
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from fix_plugin_aws.aws_client import AwsClient
2+
from typing import List, Optional
3+
from json import loads as json_loads
4+
from fixlib.types import Json
5+
6+
7+
_expected_errors = ["AccessDeniedException", "AWSOrganizationsNotInUseException"]
8+
9+
10+
def get_scps(target_id: str, client: AwsClient) -> Optional[List[Json]]:
11+
policies: List[Json] = client.list(
12+
"organizations",
13+
"list_policies_for_target",
14+
"Policies",
15+
TargetId=target_id,
16+
Filter="SERVICE_CONTROL_POLICY",
17+
expected_errors=_expected_errors,
18+
)
19+
20+
if not policies:
21+
return None
22+
23+
policy_documents = []
24+
25+
for policy in policies:
26+
policy_details = client.get(
27+
"organizations", "describe_policy", "Policy", PolicyId=policy["Id"], expected_errors=_expected_errors
28+
)
29+
if not policy_details:
30+
continue
31+
policy_document = json_loads(policy_details["Content"])
32+
policy_documents.append(policy_document)
33+
34+
return policy_documents
35+
36+
37+
def find_account_scps(client: AwsClient, account_id: str) -> List[List[Json]]:
38+
39+
def process_children(parent_id: str, parent_scps: List[List[Json]]) -> List[List[Json]]:
40+
child_ous = client.list(
41+
"organizations", "list_organizational_units_for_parent", "OrganizationalUnits", ParentId=parent_id
42+
)
43+
for child_ou in child_ous:
44+
# copy the list to avoid modifying the parent list
45+
parent_scps = list(parent_scps)
46+
org_scps = get_scps(child_ou["Id"], client)
47+
if org_scps:
48+
parent_scps.append(org_scps)
49+
accounts = client.list(
50+
"organizations",
51+
"list_accounts_for_parent",
52+
"Accounts",
53+
ParentId=child_ou["Id"],
54+
expected_errors=_expected_errors,
55+
)
56+
for account in accounts:
57+
if account["Id"] == account_id:
58+
account_scps = get_scps(account_id, client)
59+
if account_scps:
60+
parent_scps.append(account_scps)
61+
return parent_scps
62+
63+
if result := process_children(child_ou["Id"], list(parent_scps)):
64+
return result
65+
66+
return []
67+
68+
roots = client.list("organizations", "list_roots", "Roots", expected_errors=_expected_errors)
69+
for root in roots:
70+
root_id = root["Id"]
71+
parent_scps = []
72+
root_scps = get_scps(root_id, client)
73+
if root_scps:
74+
parent_scps.append(root_scps)
75+
accounts = client.list(
76+
"organizations",
77+
"list_accounts_for_parent",
78+
"Accounts",
79+
ParentId=root_id,
80+
expected_errors=_expected_errors,
81+
)
82+
# if an account is attached to the root, return early
83+
for account in accounts:
84+
if account["Id"] == account_id:
85+
account_scps = get_scps(account_id, client)
86+
if account_scps:
87+
parent_scps.append(account_scps)
88+
return parent_scps
89+
90+
if result := process_children(root["Id"], list(parent_scps)):
91+
return result
92+
93+
return []
94+
95+
96+
def is_allow_all_scp(scp: Json) -> bool:
97+
for statement in scp.get("Statement", []):
98+
if all(
99+
[
100+
statement.get("Effect") == "Allow",
101+
statement.get("Action") == "*",
102+
statement.get("Resource") == "*",
103+
]
104+
):
105+
return True
106+
107+
return False
108+
109+
110+
def filter_allow_all(levels: List[List[Json]]) -> List[List[Json]]:
111+
return [[scp for scp in level if not is_allow_all_scp(scp)] for level in levels]
112+
113+
114+
def collect_account_scps(account_id: str, scrape_org_role_arn: Optional[str], client: AwsClient) -> List[List[Json]]:
115+
116+
if scrape_org_role_arn:
117+
scp_client = AwsClient(
118+
client.config,
119+
client.account_id,
120+
role=scrape_org_role_arn,
121+
profile=client.profile,
122+
region=client.region,
123+
partition=client.partition,
124+
error_accumulator=client.error_accumulator,
125+
)
126+
else:
127+
scp_client = client
128+
129+
account_scps = find_account_scps(scp_client, account_id)
130+
account_scps = filter_allow_all(account_scps)
131+
account_scps = [level for level in account_scps if level]
132+
133+
return account_scps

0 commit comments

Comments
 (0)