├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── discover-aws-iam-resource-access.py ├── examples ├── example-external-trusts.json └── example-rds.json ├── images └── discover-aws-iam-resource-access-screenshot.png ├── requirements.txt └── src ├── __init__.py └── colorize.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | discover-aws-iam-resource-access 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | 3 | A Python script to discover AWS IAM identities (users and roles) with specified access to specified resources. 4 | 5 | Specify a list of actions and a list of resources in a parameter file, then run the script against an AWS account. 6 | Output shows the IAM users and roles that have any of the specified access to any of the specified resources, directly 7 | or indirectly. Also, shows which of the users and roles with access allow external trust entities (i.e., can be used 8 | from other accounts). 9 | 10 | # Installation 11 | 12 | No installation is required. Simply execute the discover-aws-iam-resource-access.py Python script. 13 | 14 | ### Dependencies 15 | 16 | * Python 3.9+ 17 | * Developed using Python 3.10. 18 | * Third-Party Python Libraries 19 | * Missing libraries will be identified at time of execution. 20 | * Or, see the [requirements.txt](requirements.txt) file. 21 | * Or, see the "Imports" section at the top of the discovery-aws-iam-resource-access.py file. 22 | 23 | # Usage 24 | 25 | A parameter file specifying the access and resources of interest is required: 26 | 27 | **_example-rds.json_** 28 | 29 | ``` 30 | { 31 | "ACTIONS": [ 32 | "rds:Add*", 33 | "rds:Apply*", 34 | "rds:Authorize*", 35 | "rds:Backtrack*", 36 | "rds:Copy*", 37 | "rds:Download*", 38 | "rds:Import*", 39 | "rds:Modify*", 40 | "rds:Reset*" 41 | ], 42 | 43 | "RESOURCES": [ 44 | "arn:aws:rds:us-east-1:999999999999:cluster:aurora-01", 45 | "arn:aws:rds:us-east-1:999999999999:cluster:aurora-02", 46 | "arn:aws:rds:us-east-1:999999999999:cluster:aurora-03" 47 | ] 48 | } 49 | ``` 50 | 51 | Run the script, specifying the parameter file and optionally an AWS profile name within your environment: 52 | 53 | ``` 54 | > discover-aws-iam-resource-access.py --aws-profile acme-prod-account examples/example-rds.json 55 | ``` 56 | 57 | Use the "--help" command line argument for reference: 58 | 59 | ``` 60 | >discover-aws-iam-resource-access.py --help 61 | usage: discover-aws-iam-resource-access.py [-h] [--aws-profile AWS_PROFILE] [--disable-colors] [--disable-skip] 62 | [--dev-max-roles DEV_MAX_ROLES] 63 | parameter_file 64 | 65 | Discover AWS IAM identities (users and roles) with specified access to specified resources. 66 | 67 | positional arguments: 68 | parameter_file JSON format file containing ACTIONS and RESOURCES to check. 69 | 70 | options: 71 | -h, --help show this help message and exit 72 | --aws-profile AWS_PROFILE 73 | Name of AWS profile to use for AWS API calls. 74 | --disable-colors Disable output colorization. 75 | --disable-skip Disable skipping non-essential discovery. 76 | --dev-max-roles DEV_MAX_ROLES 77 | Truncate number of roles examined to expedite development cycles. 78 | ``` 79 | 80 | # Output 81 | 82 | Tables showing access for IAM roles and users are displayed as script output: 83 | 84 | | Column | Description | 85 | | ------ | ----------- | 86 | | DIRECT | Access per identity policy. | 87 | | IAM_API | Access per iam:Attach*, iam:Create*, iam:Put*, etc. | 88 | | IAM_ROLE | Access per sts:AssumeRole or iam:PassRole. | 89 | | TRUSTS | Indicates presence of external account trust entity. | 90 | 91 | A table value of "skip" indicates that the check was skipped because the identity was already known to have another 92 | kind of access and further checking was skipped to reduce script run time. 93 | 94 | A list of IAM roles with access and their external trust entities are also listed. 95 | 96 | ### Sample 97 | 98 | ![Sample Output](images/discover-aws-iam-resource-access-screenshot.png "Sample Output") 99 | 100 | # Required Permissions 101 | 102 | The identity under which the script is executed must have the following permissions. Only identities and policies 103 | accessible to the invoker will be considered by this script. 104 | 105 | - iam:ListRoles 106 | - iam:ListUsers 107 | - iam:SimulatePrincipalPolicy 108 | 109 | # Background 110 | 111 | The AWS Management Console and similar facilities readily provide inspection of the access available to an identity. 112 | But the reverse (accesses allowed to a resource) are not readily available. This largely stems from AWS IAM being more 113 | akin to Role Based Access Control (RBAC) than to Access Control Lists (ACL). That is, access is specified on the 114 | identities and not (exclusively) on the resources. And, it is exasperated by the flexibility of the IAM policy language 115 | which allows pattern matching for access and resource names. 116 | 117 | For example, if some identities are allowed "rds:*" actions on "arn:aws:rds:us-east-1:999999999999:cluster:*-dev-*" 118 | resources, it is non-trivial to find all identities that can perform "rds:ModifyDBCluster" on the 119 | "arn:aws:rds:us-east-1:999999999999:cluster:workload-alpha-dev-01" resource. One cannot traverse from the known resource 120 | 'up' to the identities - one must inspect the access on all possible identities 'down' to the resource. 121 | 122 | # Theory of Operation 123 | 124 | The **discover-aws-iam-resource-access.py** script works approximately as follows: 125 | 126 | First, it retrieves a list of IAM roles and uses the IAM SimulatePrincipalPolicy API to check each role for direct 127 | access as specified by the parameter file. 128 | 129 | Second, it checks each role for indirect access via iam:Attach*, iam:Create*, iam:Put*, etc., any of which could be used 130 | by an identity to leverage some *other* role's direct (or indirect) access. 131 | 132 | Third, it does the same for sts:AssumeRole and iam:PassRole for the same reason. In this case, anytime an additional 133 | role is found to have indirect access, all other roles must be re-checked against this additional role. I.e., the chain 134 | of indirect access could in theory be very very long (coincidentally or as an attempt to obfuscate access). 135 | 136 | The same three methods of access are then checked for IAM users. 137 | 138 | Lastly, any role with any kind of access that allows external trust entities (other AWS accounts) to assume them are 139 | identified, since this extends the chain of access outside the current AWS account. Separate invocations of the script 140 | could be used to follow these chains, looking for specific identities within those accounts that can leverage the 141 | original account's access. 142 | 143 | # Limitations 144 | 145 | ### Resource Policies 146 | 147 | This script exclusively analyzes identity policies. It does not analyze resource policies. For some resource types, 148 | the latter can grant access, and such access would not be identified by this script. This would lead to a false 149 | negative. 150 | 151 | Please see the following for more information on resource policies and the services that support them: 152 | 153 | * [Identity-based Policies and Resource-Based Policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_identity-vs-resource.html) 154 | * [AWS Services that Work with IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html) 155 | 156 | ### IAM Policy Conditions 157 | 158 | An IAM policy statement can optionally specify conditions. For example, "allow access only when the requesting IP 159 | address is a.b.c.d". Unfortunately, for the IAM SimulatePrincipalPolicy API to determine access when conditions are 160 | present, values to evaluate those conditions must be given. I.e., the IP address for the simulated access request. 161 | It is usually impractical to exhaustively test all possible request attribute variations, and so a false negative 162 | could be encountered. Apriori knowledge of the condition would be required in order to specify request attributes 163 | matching the condition. 164 | 165 | For example: If access to a resource is allowed by a policy but only under the condition that the requester's IP 166 | address be "a.b.c.d", and the use of SimulatePrincipalPolicy does not specify an IP address attribute for the request, 167 | then SimulatePrincipalPolicy will report negative (no access). But in reality there is a condition under which the 168 | identity would have access. 169 | 170 | Separate inspection for the presence of relevant policy conditions within scope would be required to avoid false 171 | negatives due to this limitation. 172 | 173 | ### Possible vs. Actual Access 174 | 175 | This script analyzes for possible access, not actual access. An identity reported as being allowed the specified 176 | access may or may not have ever used the access. 177 | 178 | ### Point-in-Time vs. Historical Access 179 | 180 | This script performs point-in-time inspection. It is not intended for determining prior access that may now be closed. 181 | Even continuous execution of the script would not detect all access windows. A different technique entirely would 182 | be required for continuous monitoring, perhaps via monitoring of AWS Config and/or AWS CloudTrail. 183 | 184 | # Status 185 | 186 | This script is a proof-of-concept intended to illustrate an approach to the problem of determining identities with 187 | access of interest. It has not been fully tested. It has not been fully peer reviewed. Testing, consideration of its 188 | limitations, and probably fixes and improvements would be necessary to incorporate it into production scenarios. 189 | 190 | # Security 191 | 192 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 193 | 194 | # Copyright, License and Disclaimer 195 | 196 | See [LICENSE](LICENSE) and [NOTICE](NOTICE) for important copyright, license, and disclaimer information. 197 | 198 | -------------------------------------------------------------------------------- /discover-aws-iam-resource-access.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Discover AWS IAM identities (users and roles) with specified access to specified resources. 5 | """ 6 | 7 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 8 | # SPDX-License-Identifier: MIT 9 | 10 | 11 | 12 | ######################################################################################################################## 13 | # Documentation 14 | ######################################################################################################################## 15 | 16 | # See the README.md file. 17 | 18 | 19 | 20 | ######################################################################################################################## 21 | # Imports 22 | ######################################################################################################################## 23 | 24 | import sys 25 | import time 26 | import json 27 | import argparse 28 | from datetime import datetime, timezone, timedelta 29 | 30 | from src.colorize import Colorize 31 | 32 | try: 33 | import boto3 34 | import pandas 35 | import colorama 36 | except ImportError as err: 37 | print(err) 38 | print("Please install the required module (ex: 'pip install ').") 39 | exit() 40 | 41 | 42 | 43 | ######################################################################################################################## 44 | # Confirm required Python version. 45 | ######################################################################################################################## 46 | 47 | if sys.version_info < (3, 9): 48 | print("Detected Python version ", sys.version_info.major, ".", sys.version_info.minor, sep='', end='') 49 | print(", but this script requires Python 3.9+. Aborting.") 50 | exit() 51 | 52 | 53 | 54 | ######################################################################################################################## 55 | # Global Constants 56 | ######################################################################################################################## 57 | 58 | # IAM actions that indirectly provide elevated access. 59 | IAM_ROLE_ACTIONS = ['sts:AssumeRole', 'iam:PassRole'] 60 | IAM_POLICY_ACTIONS = ['iam:Attach*', 'iam:Create*', 'iam:Delete*', 'iam:Put*', 'iam:SetDefaultPolicyVersion', 'iam:Update*'] 61 | 62 | 63 | 64 | ######################################################################################################################## 65 | # Output Text Colorization 66 | ######################################################################################################################## 67 | 68 | # Defines a color for each type of output. 69 | class Color: 70 | Dev = colorama.Fore.YELLOW 71 | Info = colorama.Style.DIM + colorama.Fore.WHITE 72 | Error = colorama.Fore.RED 73 | Section = colorama.Fore.LIGHTWHITE_EX 74 | Role = colorama.Fore.WHITE 75 | ColHeader = colorama.Fore.CYAN 76 | BoolTrue = colorama.Fore.GREEN 77 | BoolFalse = colorama.Fore.RED 78 | Skip = colorama.Fore.WHITE 79 | Dot = colorama.Style.DIM + colorama.Fore.WHITE 80 | TrustEntity = colorama.Fore.LIGHTYELLOW_EX 81 | Default = Info 82 | 83 | # Defines a color for some specific known values. 84 | _color_map = { 85 | True: Color.BoolTrue, 86 | False: Color.BoolFalse, 87 | '': Color.Skip, 88 | '.': Color.Dot, 89 | } 90 | 91 | # Global output text colorization object. 92 | _colorizer = Colorize(_color_map, Color.Default) 93 | 94 | # Just a code readability aid. 95 | def Print(insie, color = None, **kwargs): 96 | print(_colorizer.Colorize(insie, color), **kwargs) 97 | 98 | # Just a code readability aid. 99 | def Write(insie, color = None): 100 | sys.stdout.write(_colorizer.Colorize(insie, color)) 101 | 102 | 103 | 104 | ######################################################################################################################## 105 | # Functions 106 | ######################################################################################################################## 107 | 108 | def GetLocalTimeZoneOffset(): 109 | """Ex: 'UTC-06:00'""" 110 | 111 | ts = time.time() 112 | dt_local = datetime.fromtimestamp(ts) 113 | dt_utc = datetime.utcfromtimestamp(ts) 114 | utc_offset_seconds = (dt_local - dt_utc).total_seconds() 115 | td = timedelta(seconds=utc_offset_seconds) 116 | tz = timezone(offset=td) 117 | 118 | return tz 119 | 120 | 121 | 122 | def GetCurrentDateTimeString(): 123 | """Ex: 'Mon Feb 28 16:05:10 2022 (UTC-06:00)'""" 124 | 125 | tz = GetLocalTimeZoneOffset() 126 | return datetime.now().strftime("%c") + ' (' + str(tz) + ')' 127 | 128 | 129 | 130 | def SimulatePrincipalPolicy(iam, arn, actions, resources): 131 | """Wrap call to IAM SimulatePrincipalPolicy API.""" 132 | 133 | # A single call to IAM SimulatePrincipalPolicy cannot check both the iam:PassRole and sts:AssumeRole actions, 134 | # as according to the error message they "require different authorization information". So, detect this case 135 | # and scatter/gather. 136 | if all(a in actions for a in IAM_ROLE_ACTIONS): 137 | 138 | result = any([SimulatePrincipalPolicy(iam, arn, [a], resources) for a in IAM_ROLE_ACTIONS]) 139 | non_iam_role_actions = [a for a in actions if a not in IAM_ROLE_ACTIONS] 140 | result = any([result, SimulatePrincipalPolicy(iam, arn, non_iam_role_actions, resources)]) 141 | 142 | else: 143 | 144 | # Checks every combination of [actions] and [resources]. 145 | rsp = iam.simulate_principal_policy( 146 | PolicySourceArn=arn, 147 | ActionNames=actions, 148 | ResourceArns=resources 149 | ) 150 | 151 | # Any action allowed for any resource is considered 'allowed' for our purposes. 152 | result = any([(r['EvalDecision'] == 'allowed') for r in rsp['EvaluationResults']]) 153 | 154 | return result 155 | 156 | 157 | 158 | def StatementAllowsAssumeRoleForAwsPrincipal(s): 159 | """Does the given statement allow sts:AssumeRole for the 'AWS' principal?""" 160 | 161 | result = s['Effect'] == 'Allow' and s['Action'] == 'sts:AssumeRole' and 'AWS' in s['Principal'] 162 | 163 | return result 164 | 165 | 166 | 167 | def PrintRoleTrustPolicyInfo(role): 168 | """Debugging aid.""" 169 | 170 | o = role['RoleName'] + " Trust Policy:\n" 171 | 172 | for s in role['AssumeRolePolicyDocument']['Statement']: 173 | o += " " + s['Effect'] + " " + s['Action'] + " by " + str(s['Principal']) 174 | if 'Condition' in s: 175 | o += " when " + str(s['Condition']) 176 | o += "\n" 177 | 178 | Print(o, Color.Dev) 179 | 180 | 181 | 182 | def PrintTable(table): 183 | """Pretty-print a colorized table.""" 184 | 185 | # Make a copy with colorized column headers. 186 | colorized_table = {} 187 | 188 | for rn in table: 189 | colorized_table[rn] = {} 190 | for cn in table[rn]: 191 | colorized_table[rn][_colorizer.Colorize(cn, Color.ColHeader)] = table[rn][cn] 192 | 193 | # Use pandas to format the table, colorizing the elements along the way. 194 | df = pandas.DataFrame(colorized_table).T.applymap(_colorizer.Colorize) 195 | 196 | print(df) 197 | 198 | 199 | 200 | def CheckParam(p, name, location): 201 | """Check an input parameter ('None', empty, all whitespace, non-printable characters, ...)""" 202 | 203 | if p is None or p == '' or p.isspace(): 204 | raise Exception("Empty '" + name + "' value found in '" + location + "'.") 205 | 206 | if not p.isprintable(): 207 | raise Exception("Invalid '" + name + "' value found in '" + location + "': '" + p + "'") 208 | 209 | 210 | 211 | ######################################################################################################################## 212 | # Main Script 213 | ######################################################################################################################## 214 | 215 | def main(): 216 | 217 | # 218 | # Parse Command Line Arguments 219 | # 220 | 221 | parser = argparse.ArgumentParser( 222 | description = 'Discover AWS IAM identities (users and roles) with specified access to specified resources.') 223 | 224 | parser.add_argument( 225 | 'parameter_file', 226 | help = 'JSON format file containing ACTIONS and RESOURCES to check.') 227 | 228 | parser.add_argument( 229 | '--aws-profile', 230 | help = 'Name of AWS profile to use for AWS API calls.') 231 | 232 | parser.add_argument( 233 | '--disable-colors', 234 | action = 'store_true', 235 | help = 'Disable output colorization.') 236 | 237 | parser.add_argument( 238 | '--disable-skip', 239 | action = 'store_true', 240 | help = 'Disable skipping non-essential discovery.') 241 | 242 | parser.add_argument( 243 | '--dev-max-roles', 244 | type = int, 245 | help = 'Truncate number of roles examined to expedite development cycles.') 246 | 247 | args = parser.parse_args() 248 | 249 | if args.disable_colors: 250 | _colorizer.Enabled = False 251 | 252 | # 253 | # Load Parameter File 254 | # 255 | 256 | try: 257 | 258 | with open(args.parameter_file, "r") as f: 259 | params = json.load(f) 260 | 261 | if 'ACTIONS' not in params: 262 | raise Exception("Required parameter 'ACTIONS' not found in '" + args.parameter_file + "'.") 263 | 264 | [CheckParam(p, 'ACTIONS', args.parameter_file) for p in params['ACTIONS']] 265 | 266 | if 'RESOURCES' not in params: 267 | raise Exception("Required parameter 'RESOURCES' not found in '" + args.parameter_file + "'.") 268 | 269 | [CheckParam(p, 'RESOURCES', args.parameter_file) for p in params['RESOURCES']] 270 | 271 | except Exception as err: 272 | 273 | Print(err, Color.Error) 274 | exit() 275 | 276 | setattr(args, 'actions', params['ACTIONS']) 277 | setattr(args, 'resources', params['RESOURCES']) 278 | 279 | # 280 | # Init 281 | # 282 | 283 | # Boto3 is a Python SDK for accessing AWS APIs. 284 | boto3.setup_default_session(profile_name = args.aws_profile) 285 | iam = boto3.client('iam') 286 | 287 | # Pandas is used to format output as a table. 288 | pandas.set_option('colheader_justify', 'center') 289 | 290 | # 291 | # Intro 292 | # 293 | 294 | Print('') 295 | Print('Current Time:') 296 | Print(' ' + GetCurrentDateTimeString()) 297 | Print('') 298 | Print('Input Parameters:') 299 | Print(' Account: ' + boto3.client('sts').get_caller_identity().get('Account')) 300 | Print(' Actions: ' + str(args.actions)) 301 | Print(' Resources: ' + str(args.resources)) 302 | Print('') 303 | 304 | all_roles = [] 305 | role_results = {} 306 | allowed_roles = set() 307 | all_users = [] 308 | user_results = {} 309 | 310 | # 311 | # All the Real Work... 312 | # 313 | 314 | Print("Retrieving list of IAM Roles...", end=''), 315 | 316 | for rsp in iam.get_paginator('list_roles').paginate(): 317 | for role in rsp['Roles']: 318 | all_roles.append(role) 319 | 320 | Print("(Found " + str(len(all_roles)) + ")") 321 | 322 | if args.dev_max_roles: 323 | msg = " Truncating to --dev-max-roles = " + str(args.dev_max_roles) 324 | Print(msg, Color.Dev) 325 | del all_roles[args.dev_max_roles:] 326 | 327 | 328 | 329 | Print('') 330 | Print("Checking each IAM Role for " + str(args.actions), end='') 331 | 332 | for role in all_roles: 333 | 334 | role_results[role['RoleName']] = {} 335 | result = SimulatePrincipalPolicy(iam, role['Arn'], args.actions, args.resources) 336 | role_results[role['RoleName']]['DIRECT'] = result 337 | if result: 338 | allowed_roles.add(role['Arn']) 339 | Write(".") 340 | sys.stdout.flush() 341 | 342 | 343 | 344 | Print('') 345 | Print("Checking each IAM Role for " + str(IAM_POLICY_ACTIONS), end=''), 346 | 347 | for role in all_roles: 348 | 349 | result = SimulatePrincipalPolicy(iam, role['Arn'], IAM_POLICY_ACTIONS, ['*']) 350 | role_results[role['RoleName']]['IAM_API'] = result 351 | if result: 352 | allowed_roles.add(role['Arn']) 353 | Write(".") 354 | sys.stdout.flush() 355 | 356 | 357 | 358 | Print('') 359 | Print("Checking each IAM Role for " + str(IAM_ROLE_ACTIONS), end=''), 360 | 361 | # Must repeat until no additional roles with access are discovered. 362 | 363 | allowed_roles_cnt = 0 364 | 365 | # While the allowed_roles count increased on the last iteration... 366 | while allowed_roles_cnt != len(allowed_roles): 367 | 368 | if allowed_roles_cnt != 0: 369 | Print("Rechecking...") 370 | 371 | allowed_roles_cnt = len(allowed_roles) 372 | 373 | for role in all_roles: 374 | 375 | if not args.disable_skip and role['Arn'] in allowed_roles: 376 | # Skip checking roles that are already known to have access (saves time). 377 | role_results[role['RoleName']]['IAM_ROLE'] = '' 378 | else: 379 | result = SimulatePrincipalPolicy(iam, role['Arn'], IAM_ROLE_ACTIONS, list(allowed_roles)) 380 | role_results[role['RoleName']]['IAM_ROLE'] = result 381 | if result: 382 | allowed_roles.add(role['Arn']) 383 | Write(".") 384 | sys.stdout.flush() 385 | 386 | 387 | 388 | Print('') 389 | Print("Checking Trusted Entities for each IAM Role with access", end=''), 390 | 391 | for role in all_roles: 392 | 393 | if not args.disable_skip and role['Arn'] not in allowed_roles: 394 | # Skip checking roles that do not have access (saves time). 395 | role_results[role['RoleName']]['TRUSTS'] = '' 396 | else: 397 | rs = role['AssumeRolePolicyDocument']['Statement'] 398 | result = any([(StatementAllowsAssumeRoleForAwsPrincipal(s)) for s in rs]) 399 | role_results[role['RoleName']]['TRUSTS'] = result 400 | Write(".") 401 | sys.stdout.flush() 402 | 403 | 404 | 405 | Print('') 406 | Print('') 407 | Print("IAM Role Results:", Color.Section) 408 | Print('') 409 | 410 | PrintTable(role_results) 411 | 412 | 413 | 414 | Print('') 415 | Print("Retrieving list of IAM Users...", end=''), 416 | 417 | for rsp in iam.get_paginator('list_users').paginate(): 418 | for user in rsp['Users']: 419 | all_users.append(user) 420 | 421 | Print("(Found " + str(len(all_users)) + ")") 422 | 423 | 424 | 425 | Print('') 426 | Print("Checking each IAM User for " + str(args.actions), end='') 427 | 428 | for user in all_users: 429 | 430 | user_results[user['UserName']] = {} 431 | result = SimulatePrincipalPolicy(iam, user['Arn'], args.actions, args.resources) 432 | user_results[user['UserName']]['DIRECT'] = result 433 | Write(".") 434 | sys.stdout.flush() 435 | 436 | 437 | 438 | Print('') 439 | Print("Checking each IAM User for " + str(IAM_POLICY_ACTIONS), end=''), 440 | 441 | for user in all_users: 442 | 443 | result = SimulatePrincipalPolicy(iam, user['Arn'], IAM_POLICY_ACTIONS, ['*']) 444 | user_results[user['UserName']]['IAM_API'] = result 445 | Write(".") 446 | sys.stdout.flush() 447 | 448 | 449 | 450 | Print('') 451 | Print("Checking each IAM User for " + str(IAM_ROLE_ACTIONS), end=''), 452 | 453 | for user in all_users: 454 | 455 | result = SimulatePrincipalPolicy(iam, user['Arn'], IAM_ROLE_ACTIONS, list(allowed_roles)) 456 | user_results[user['UserName']]['IAM_ROLE'] = result 457 | Write(".") 458 | sys.stdout.flush() 459 | 460 | 461 | 462 | Print('') 463 | Print('') 464 | Print("IAM User Results:", Color.Section) 465 | Print('') 466 | 467 | PrintTable(user_results) 468 | 469 | 470 | 471 | Print('') 472 | Print('') 473 | Print("IAM Role External Account Trust Entities:", Color.Section) 474 | Print('') 475 | 476 | for role in all_roles: 477 | 478 | if role['Arn'] in allowed_roles and role_results[role['RoleName']]['TRUSTS']: 479 | Print(role['RoleName'], Color.Role, end='') 480 | Print(" (" + role['Arn'] + ")", Color.Info) 481 | 482 | for s in role['AssumeRolePolicyDocument']['Statement']: 483 | 484 | if StatementAllowsAssumeRoleForAwsPrincipal(s): 485 | Print(" " + s['Principal']['AWS'].removeprefix("arn:aws:iam::"), Color.TrustEntity) 486 | 487 | 488 | 489 | ######################################################################################################################## 490 | # See: https://docs.python.org/3/library/__main__.html#idiomatic-usage 491 | ######################################################################################################################## 492 | 493 | if __name__ == '__main__': 494 | sys.exit(main()) 495 | -------------------------------------------------------------------------------- /examples/example-external-trusts.json: -------------------------------------------------------------------------------- 1 | { 2 | "ACTIONS": [ 3 | "iam:AssumeRole", 4 | "iam:PassRole" 5 | ], 6 | 7 | "RESOURCES": [ 8 | "arn:aws:iam::999999999999:role/AuroraRoleFull" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/example-rds.json: -------------------------------------------------------------------------------- 1 | { 2 | "ACTIONS": [ 3 | "rds:Add*", 4 | "rds:Apply*", 5 | "rds:Authorize*", 6 | "rds:Backtrack*", 7 | "rds:Copy*", 8 | "rds:Download*", 9 | "rds:Import*", 10 | "rds:Modify*", 11 | "rds:Reset*" 12 | ], 13 | 14 | "RESOURCES": [ 15 | "arn:aws:rds:us-east-1:999999999999:cluster:aurora-01", 16 | "arn:aws:rds:us-east-1:999999999999:cluster:aurora-02", 17 | "arn:aws:rds:us-east-1:999999999999:cluster:aurora-03" 18 | ] 19 | } -------------------------------------------------------------------------------- /images/discover-aws-iam-resource-access-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/discover-aws-iam-resource-access/57649a92434b384857a4a61e08a69c2f094f0cbe/images/discover-aws-iam-resource-access-screenshot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3>=1.21.6 2 | colorama>=0.4.4 3 | pandas>=1.4.1 4 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/discover-aws-iam-resource-access/57649a92434b384857a4a61e08a69c2f094f0cbe/src/__init__.py -------------------------------------------------------------------------------- /src/colorize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines a simple helper class on top of colorama for stateful string colorization. 3 | """ 4 | 5 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 6 | # SPDX-License-Identifier: MIT 7 | 8 | 9 | 10 | ######################################################################################################################## 11 | # Imports 12 | ######################################################################################################################## 13 | 14 | try: 15 | import colorama 16 | except ImportError as err: 17 | print(err) 18 | print("Please install the required module (ex: 'pip install ').") 19 | exit() 20 | 21 | 22 | 23 | ######################################################################################################################## 24 | # Main Class 25 | ######################################################################################################################## 26 | 27 | class Colorize: 28 | """ 29 | Simple helper class on top of colorama to always append colorama.Style.RESET_ALL and hold state: 30 | - Enable/Disable Colorization 31 | - Value-to-Color Map to Auto-Colorize Known Values 32 | - Default Color for Unknown Values 33 | """ 34 | 35 | Enabled = True 36 | ColorMap = None 37 | DefaultColor = None 38 | 39 | def __init__(self, color_map = None, default_color = None, enabled = True): 40 | """ 41 | :param color_map: Optional value-to-color map for auto-colorization. 42 | :param default_color: Optional color for unknown values. 43 | :param enabled: Colorization is only performed when enabled. 44 | """ 45 | 46 | self.Enabled = enabled 47 | self.ColorMap = color_map 48 | self.DefaultColor = default_color 49 | 50 | def Colorize(self, insie, color = None): 51 | """ 52 | Return the input string colorized by (in order) 'color' parameter, or 'color_map' lookup, or 'default_color'. 53 | 54 | :param insie: Value to colorize (and convert to string as necessary). 55 | :param color: Color to use (omit to use 'color_map' or 'default_color'). 56 | :return: Colorized input value. 57 | """ 58 | 59 | if not self.Enabled: 60 | outsie = insie 61 | 62 | elif insie is None or insie == '': 63 | outsie = insie 64 | 65 | elif color: 66 | outsie = color + str(insie) + colorama.Style.RESET_ALL 67 | 68 | else: 69 | outsie = self.ColorMap.get(insie, self.DefaultColor) + str(insie) + colorama.Style.RESET_ALL 70 | 71 | return outsie 72 | 73 | 74 | 75 | ######################################################################################################################## 76 | # Basic Testing 77 | ######################################################################################################################## 78 | 79 | if __name__ == "__main__": 80 | 81 | print() 82 | print("Running Colorize test cases...") 83 | print() 84 | 85 | import sys 86 | 87 | class Color: 88 | Dev = colorama.Fore.YELLOW 89 | Info = colorama.Style.DIM + colorama.Fore.WHITE 90 | Error = colorama.Fore.RED 91 | BoolTrue = colorama.Fore.GREEN 92 | BoolFalse = colorama.Fore.RED 93 | Dot = colorama.Style.DIM + colorama.Fore.WHITE 94 | Default = colorama.Fore.CYAN 95 | 96 | color_map = { 97 | 'Dev': Color.Dev, 98 | 'Info': Color.Info, 99 | 'Error': Color.Error, 100 | '.': Color.Dot, 101 | True: Color.BoolTrue, 102 | False: Color.BoolFalse, 103 | } 104 | 105 | c = Colorize(color_map, Color.Default) 106 | 107 | print(c.Colorize("Error", colorama.Fore.CYAN) + " <-- Should be CYAN.") 108 | 109 | print(c.Colorize(None), "<-- Should not be colorized.") 110 | print(c.Colorize('') + "<-- Should be empty string.") 111 | 112 | print(c.Colorize("Dev")) 113 | print(c.Colorize("Info")) 114 | print(c.Colorize("Error")) 115 | print(c.Colorize(True)) 116 | print(c.Colorize(False)) 117 | 118 | print(c.Colorize("Should be the default color (CYAN).")) 119 | 120 | for i in range (0, 10): 121 | sys.stdout.write(c.Colorize('.')) 122 | print(" <-- Should be single line of 10 dark-gray dots.") 123 | 124 | c.Enabled = False 125 | 126 | print(c.Colorize("This line should not be colorized.", Color.Error)) 127 | --------------------------------------------------------------------------------