Skip to content

Commit b502352

Browse files
ncc-erik-steringerErik Steringer
and
Erik Steringer
authored
v1.1.4 (#99)
* Added preset for service access * Added preset for service access * fix command line output in orgs subcommand * added SCP support to admin-checks * added SCP support to admin-checks * cutting down on Lambda authorization simulations * Initial work on resources and potential confused deputy risks * implemented support for botocore client generation with custom arguments, added localstack endpoint support for "graph create" subcommand * implemented a fix for #98 - cloudformation:UpdateStack risks * implemented a fix for #97 - checking for Login Profile for full password check * progress on datapipeline work * hotfix * pulling datapipeline until 1.1.5 Co-authored-by: Erik Steringer <erik.steringer@nccgroup.com>
1 parent 722efec commit b502352

24 files changed

+426
-91
lines changed

principalmapper/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
# You should have received a copy of the GNU Affero General Public License
1616
# along with Principal Mapper. If not, see <https://www.gnu.org/licenses/>.
1717

18-
__version__ = '1.1.3'
18+
__version__ = '1.1.4'

principalmapper/analysis/find_risks.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@
2828

2929
import datetime as dt
3030
import json
31-
from typing import List, Optional
31+
from typing import List, Optional, Tuple
3232

3333
import principalmapper
3434
from principalmapper.analysis.finding import Finding
3535
from principalmapper.analysis.report import Report
3636
from principalmapper.common import Graph, Node, Edge
3737
from principalmapper.querying import query_interface
38+
from principalmapper.querying.local_policy_simulation import resource_policy_authorization, ResourcePolicyEvalResult
3839
from principalmapper.querying.presets.privesc import can_privesc
3940
from principalmapper.util import arns
4041

@@ -447,6 +448,74 @@ def gen_admin_users_without_mfa_finding(graph: Graph) -> List[Finding]:
447448
return result
448449

449450

451+
def gen_resources_with_potential_confused_deputies(graph: Graph) -> List[Finding]:
452+
"""Generates findings related to AWS resources that allow access to AWS services (via resource policy)
453+
that may not correctly verify which AWS account is the true source of a request that
454+
affects the given resource.
455+
456+
Primarily works by inspecting resource policies and making sure that access is guarded
457+
with a condition using aws:SourceAccount."""
458+
459+
result = []
460+
461+
resource_service_action_map = {
462+
's3': {
463+
'serverlessrepo.amazonaws.com': [
464+
's3:GetObject'
465+
]
466+
}
467+
}
468+
469+
affected_policies = [] # type: List[Tuple[str, str, str]]
470+
for resource_type in resource_service_action_map.keys():
471+
for policy in graph.policies:
472+
if arns.get_service(policy.arn) == resource_type:
473+
for service, action_list in resource_service_action_map[resource_type].items():
474+
available_actions = []
475+
for action in action_list:
476+
rpa_result = resource_policy_authorization(
477+
service,
478+
graph.metadata['account_id'],
479+
policy.policy_doc,
480+
action,
481+
policy.arn,
482+
{
483+
'aws:SourceAccount': '000000000000'
484+
}
485+
)
486+
if rpa_result.SERVICE_MATCH:
487+
available_actions.append(action)
488+
if len(available_actions) > 0:
489+
affected_policies.append(
490+
(policy.arn, service, ' | '.join(available_actions))
491+
)
492+
493+
if len(affected_policies) > 0:
494+
desc_list_str = '\n'.join(['* With service {}, the resource {} for the action(s): {}'.format(y, x, z) for x, y, z in affected_policies])
495+
result.append(
496+
Finding(
497+
'Resources With A Potential Confused-Deputy Risk',
498+
'Medium',
499+
'Depending on the affected resources and services, an attacker may be able to execute read or write '
500+
'operations on the resources from another AWS account.',
501+
'In AWS, certain services will create and use resources in the customer\'s own AWS account. This may '
502+
'be controlled using a resource policy that grants access to the service that created the resource '
503+
'in the customer\'s AWS account. However, some services require customers to use the '
504+
'`${aws:SourceAccount}` condition context key to control access to the account resource from the '
505+
'service. In other words, to prevent the service from accessing the resource on the behalf of '
506+
'another customer, the resource needs a resource policy that allow-lists the true "source" of a '
507+
'request.\n\n'
508+
'The following AWS services and resources could allow an external account to potentially gain '
509+
'read/write access to the resources:\n\n' + desc_list_str,
510+
'Update the resource policy for all affected resources, and ensure that all statements granting '
511+
'access to AWS services check against the `${aws:SourceAccount}` condition context key when '
512+
'appropriate.'
513+
)
514+
)
515+
516+
return result
517+
518+
450519
def print_report(report: Report) -> None:
451520
"""Given a report, uses print() to print out their contents in a Markdown format."""
452521

principalmapper/graphing/autoscaling_edges.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,20 @@ class AutoScalingEdgeChecker(EdgeChecker):
3434
"""Class for identifying if EC2 Auto Scaling can be used by IAM principals to gain access to other IAM principals."""
3535

3636
def return_edges(self, nodes: List[Node], region_allow_list: Optional[List[str]] = None,
37-
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None) -> List[Edge]:
37+
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None,
38+
client_args_map: Optional[dict] = None) -> List[Edge]:
3839
"""Fulfills expected method return_edges."""
3940

4041
logger.info('Generating Edges based on EC2 Auto Scaling.')
4142

43+
asargs = client_args_map.get('autoscaling', {})
44+
4245
# Gather projects information for each region
4346
autoscaling_clients = []
4447
if self.session is not None:
4548
as_regions = botocore_tools.get_regions_to_search(self.session, 'autoscaling', region_allow_list, region_deny_list)
4649
for region in as_regions:
47-
autoscaling_clients.append(self.session.create_client('autoscaling', region_name=region))
50+
autoscaling_clients.append(self.session.create_client('autoscaling', region_name=region, **asargs))
4851

4952
launch_configs = []
5053
for as_client in autoscaling_clients:

principalmapper/graphing/cloudformation_edges.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,20 @@ class CloudFormationEdgeChecker(EdgeChecker):
3636
"""Class for identifying if CloudFormation can be used by IAM principals to gain access to other IAM principals."""
3737

3838
def return_edges(self, nodes: List[Node], region_allow_list: Optional[List[str]] = None,
39-
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None) -> List[Edge]:
39+
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None,
40+
client_args_map: Optional[dict] = None) -> List[Edge]:
4041
"""Fulfills expected method return_edges."""
4142

4243
logger.info('Pulling data on CloudFormation stacks.')
4344

45+
cfargs = client_args_map.get('cloudformation', {})
46+
4447
# Grab existing stacks in each region
4548
cloudformation_clients = []
4649
if self.session is not None:
4750
cf_regions = botocore_tools.get_regions_to_search(self.session, 'cloudformation', region_allow_list, region_deny_list)
4851
for region in cf_regions:
49-
cloudformation_clients.append(self.session.create_client('cloudformation', region_name=region))
52+
cloudformation_clients.append(self.session.create_client('cloudformation', region_name=region, **cfargs))
5053

5154
# grab existing cloudformation stacks
5255
stack_list = []
@@ -136,7 +139,7 @@ def generate_edges_locally(nodes: List[Node], stack_list: List[dict], scps: Opti
136139

137140
relevant_stacks = [] # we'll reuse this for *ChangeSet
138141
for stack in stack_list:
139-
if 'RoleArn' in stack:
142+
if 'RoleARN' in stack:
140143
if stack['RoleARN'] == node_destination.arn:
141144
relevant_stacks.append(stack)
142145

principalmapper/graphing/codebuild_edges.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,21 @@ class CodeBuildEdgeChecker(EdgeChecker):
3434
"""Class for identifying if CodeBuild can be used by IAM principals to gain access to other IAM principals."""
3535

3636
def return_edges(self, nodes: List[Node], region_allow_list: Optional[List[str]] = None,
37-
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None) -> List[Edge]:
37+
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None,
38+
client_args_map: Optional[dict] = None) -> List[Edge]:
3839
"""Fulfills expected method return_edges."""
3940

4041
logger.info('Generating Edges based on CodeBuild.')
4142

4243
# Gather projects information for each region
4344

45+
cbargs = client_args_map.get('codebuild', {})
46+
4447
codebuild_clients = []
4548
if self.session is not None:
4649
cf_regions = botocore_tools.get_regions_to_search(self.session, 'codebuild', region_allow_list, region_deny_list)
4750
for region in cf_regions:
48-
codebuild_clients.append(self.session.create_client('codebuild', region_name=region))
51+
codebuild_clients.append(self.session.create_client('codebuild', region_name=region, **cbargs))
4952

5053
codebuild_projects = []
5154
for cb_client in codebuild_clients:

principalmapper/graphing/ec2_edges.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class EC2EdgeChecker(EdgeChecker):
3535
"""Class for identifying if EC2 can be used by IAM principals to gain access to other IAM principals."""
3636

3737
def return_edges(self, nodes: List[Node], region_allow_list: Optional[List[str]] = None,
38-
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None) -> List[Edge]:
38+
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None,
39+
client_args_map: Optional[dict] = None) -> List[Edge]:
3940
"""Fulfills expected method return_edges."""
4041

4142
logger.info('Generating Edges based on EC2.')

principalmapper/graphing/edge_checker.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def __init__(self, session: botocore.session.Session):
3232
self.session = session
3333

3434
def return_edges(self, nodes: List[Node], region_allow_list: Optional[List[str]] = None,
35-
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None) -> List[Edge]:
35+
region_deny_list: Optional[List[str]] = None, scps: Optional[List[List[dict]]] = None,
36+
client_args_map: Optional[dict] = None) -> List[Edge]:
3637
"""Subclasses shall override this method. Given a list of nodes, the EdgeChecker should be able to use its session
3738
object in order to make clients and call the AWS API to resolve information about the account. Then,
3839
with this information, it should return a list of edges between the passed nodes.

principalmapper/graphing/edge_identification.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151

5252
def obtain_edges(session: Optional[botocore.session.Session], checker_list: List[str], nodes: List[Node],
5353
region_allow_list: Optional[List[str]] = None, region_deny_list: Optional[List[str]] = None,
54-
scps: Optional[List[List[dict]]] = None) -> List[Edge]:
54+
scps: Optional[List[List[dict]]] = None, client_args_map: Optional[dict] = None) -> List[Edge]:
5555
"""Given a list of nodes and a botocore Session, return a list of edges between those nodes. Only checks
5656
against services passed in the checker_list param. """
5757
result = []
@@ -60,5 +60,5 @@ def obtain_edges(session: Optional[botocore.session.Session], checker_list: List
6060
for check in checker_list:
6161
if check in checker_map:
6262
checker_obj = checker_map[check](session)
63-
result.extend(checker_obj.return_edges(nodes, region_allow_list, region_deny_list, scps))
63+
result.extend(checker_obj.return_edges(nodes, region_allow_list, region_deny_list, scps, client_args_map))
6464
return result

0 commit comments

Comments
 (0)