├── .gitignore ├── requirements.txt ├── trusted_accounts.yaml.sample ├── LICENSE.md ├── README.md └── check.py /.gitignore: -------------------------------------------------------------------------------- 1 | aws_account_analysis* -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3>=1.28.0 2 | pyyaml>=6.0 3 | requests>=2.28.0 4 | rich>=13.3.0 5 | botocore>=1.31.0 -------------------------------------------------------------------------------- /trusted_accounts.yaml.sample: -------------------------------------------------------------------------------- 1 | # Trusted Accounts Configuration 2 | # This is a sample file for configuring trusted AWS accounts in your organization. 3 | # Rename this file to 'trusted_accounts.yaml' to use it. 4 | 5 | # Each entry represents a group of trusted AWS accounts 6 | # Format follows the same pattern as the known AWS accounts reference 7 | 8 | - name: 'My Company Production' 9 | description: 'Production AWS accounts' 10 | accounts: 11 | - '123456789012' 12 | - '234567890123' 13 | 14 | - name: 'My Company Development' 15 | description: 'Development and testing AWS accounts' 16 | accounts: 17 | - '345678901234' 18 | - '456789012345' 19 | 20 | - name: 'Subsidiary Company' 21 | description: 'Our subsidiary organization AWS accounts' 22 | accounts: 23 | - '567890123456' 24 | 25 | # Add more entries as needed -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Know Your Enemies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Know Your Enemies - AWS Account Analysis Tool 2 | 3 | This tool analyzes IAM Role trust policies and S3 bucket policies in your AWS account to identify third-party vendors with access to your resources. It compares the AWS account IDs found in these policies against a reference list of [known AWS accounts from fwd:cloudsec](https://github.com/fwdcloudsec/known_aws_accounts/) to identify the vendors behind these accounts. 4 | 5 | ## Features 6 | 7 | - 🔍 Analyzes IAM Role trust policies to identify who can assume your roles 8 | - 🔍 Checks S3 bucket policies to identify who has access to your data 9 | - 📊 Uses reference data from [known AWS accounts](https://github.com/fwdcloudsec/known_aws_accounts) to identify vendors 10 | - 🔒 Supports defining your own trusted AWS accounts to distinguish between internal and external access 11 | - 🏷️ Automatically detects and displays AWS account aliases for better readability 12 | - ⚠️ Identifies IAM roles vulnerable to the confused deputy problem (missing ExternalId condition) 13 | - 📝 Generates nice-looking console output with tables 14 | - 📄 Creates a markdown report you can share with your security team 15 | 16 | ## Installation 17 | 18 | 1. Clone this repository: 19 | 20 | ``` 21 | git clone https://github.com/yourusername/know-your-enemies.git 22 | cd know-your-enemies 23 | ``` 24 | 25 | 2. Install the required dependencies: 26 | 27 | ``` 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | 3. Configure your AWS credentials: 32 | ``` 33 | aws configure 34 | ``` 35 | or set environment variables: 36 | ``` 37 | export AWS_ACCESS_KEY_ID="your-access-key" 38 | export AWS_SECRET_ACCESS_KEY="your-secret-key" 39 | export AWS_DEFAULT_REGION="your-region" 40 | ``` 41 | 42 | ## Required AWS Permissions 43 | 44 | To run this script successfully, your AWS user or role needs the following permissions: 45 | 46 | ```json 47 | { 48 | "Version": "2012-10-17", 49 | "Statement": [ 50 | { 51 | "Effect": "Allow", 52 | "Action": ["iam:ListRoles", "iam:GetRole"], 53 | "Resource": "*" 54 | }, 55 | { 56 | "Effect": "Allow", 57 | "Action": ["s3:ListAllMyBuckets", "s3:GetBucketPolicy"], 58 | "Resource": "*" 59 | }, 60 | { 61 | "Effect": "Allow", 62 | "Action": ["sts:GetCallerIdentity", "iam:ListAccountAliases"], 63 | "Resource": "*" 64 | }, 65 | { 66 | "Effect": "Allow", 67 | "Action": ["organizations:ListAccounts"], 68 | "Resource": "*" 69 | } 70 | ] 71 | } 72 | ``` 73 | 74 | You can use the AWS built-in policies: 75 | 76 | - `IAMReadOnlyAccess` - For IAM role analysis 77 | - `AmazonS3ReadOnlyAccess` - For S3 bucket policy analysis 78 | - `AWSOrganizationsReadOnlyAccess` - For AWS Organizations account listing 79 | 80 | Or create a custom policy with just the permissions listed above for more restricted access. 81 | 82 | ## Trusted Accounts Configuration 83 | 84 | You can define your own trusted AWS accounts to distinguish between your internal organization's accounts and external vendors. This helps you focus on identifying truly external access. 85 | 86 | 1. Create a `trusted_accounts.yaml` file in the same directory as the script: 87 | 88 | ``` 89 | cp trusted_accounts.yaml.sample trusted_accounts.yaml 90 | ``` 91 | 92 | 2. Edit the file to include your organization's AWS accounts: 93 | 94 | ```yaml 95 | - name: "My Company Production" 96 | description: "Production AWS accounts" 97 | accounts: 98 | - "123456789012" 99 | - "234567890123" 100 | 101 | - name: "My Company Development" 102 | description: "Development AWS accounts" 103 | accounts: 104 | - "345678901234" 105 | ``` 106 | 107 | If the `trusted_accounts.yaml` file doesn't exist or is empty, the script will analyze all accounts as potential external access points. 108 | 109 | ## Security Checks Performed 110 | 111 | ### Confused Deputy Problem Detection 112 | 113 | The tool checks if IAM roles with cross-account access are properly protected with an ExternalId condition. The ExternalId condition helps prevent the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html), which occurs when a third-party (the deputy) is tricked into misusing its access to act on behalf of another account. 114 | 115 | Roles that allow external accounts to assume them without an ExternalId condition are flagged as vulnerable in the report. 116 | 117 | ## Usage 118 | 119 | Simply run the script: 120 | 121 | ``` 122 | python check.py 123 | ``` 124 | 125 | The script will: 126 | 127 | 1. Fetch the latest reference data of known AWS accounts 128 | 2. Load any trusted accounts from your configuration (if available) 129 | 3. Get the current AWS account alias for better identification 130 | 4. Check all IAM role trust policies in your account 131 | 5. Check all S3 bucket policies in your account 132 | 6. Identify IAM roles vulnerable to the confused deputy problem 133 | 7. Display the results in a nice format in the console 134 | 8. Generate a markdown report file 135 | 136 | ## Sample Output 137 | 138 | ``` 139 | ┌─────────────────────── 🔍 AWS Analysis ───────────────────────┐ 140 | │ Know Your Enemies - AWS Account Analysis Tool │ 141 | │ This tool analyzes IAM Role trust policies and S3 bucket │ 142 | │ policies to identify third-party vendors with access to your │ 143 | │ resources. │ 144 | └───────────────────────────────────────────────────────────────┘ 145 | 146 | Fetching reference data of known AWS accounts... 147 | ✅ Found 480 known AWS accounts in the reference data 148 | Loading trusted AWS accounts... 149 | ✅ Loaded 5 trusted AWS accounts 150 | 151 | Analyzing AWS Account: 123456789012 (my-company-dev) 152 | 153 | Checking IAM role trust policies... 154 | Checking S3 bucket policies... 155 | 156 | ┌────────── 🔒 Trusted Entities with IAM Role Access ───────────────┐ 157 | │ Entity │ IAM Roles │ 158 | │ ─────────────────┼─────────────────────────────────────────────── │ 159 | │ My Company Prod │ CrossAccountRole │ 160 | └───────────────────────────────────────────────────────────────────┘ 161 | 162 | ┌────────────── ✅ Known Vendors with IAM Role Access ────────────┐ 163 | │ Vendor │ IAM Roles │ 164 | │ ─────────────────┼───────────────────────────────────────────── │ 165 | │ Datadog │ DatadogIntegrationRole │ 166 | └─────────────────────────────────────────────────────────────────┘ 167 | 168 | ┌───── ❓ Unknown AWS Accounts with IAM Role Access ─────────────┐ 169 | │ AWS Account ID │ IAM Roles │ 170 | │ ────────────────┼───────────────────────────────────────────── │ 171 | │ 123456789012 │ SomeUnknownVendorRole │ 172 | └────────────────────────────────────────────────────────────────┘ 173 | 174 | ┌──── ⚠️ Missing ExternalId Condition (VConfused Deputy) ──┐ 175 | │ Entity │ Vulnerable IAM Roles │ 176 | │ ───────────────┼──────────────────────────────────────── │ 177 | │ Datadog │ DatadogIntegrationRole │ 178 | └──────────────────────────────────────────────────────────┘ 179 | 180 | ┌────────────────── AWS Account Analysis Results ───────────────────┐ 181 | │ Summary: │ 182 | │ 🔒 Trusted entities found: 1 │ 183 | │ 🔍 Known vendors found: 1 │ 184 | │ ❓ Unknown AWS accounts found: 1 │ 185 | │ ⚠️ Vulnerable IAM roles (missing ExternalId): 1 │ 186 | └───────────────────────────────────────────────────────────────────┘ 187 | 188 | ✅ Report generated: aws_account_analysis_20230515_123045.md 189 | ``` 190 | 191 | ## Report Format 192 | 193 | The generated markdown report will include: 194 | 195 | - Trusted entities with IAM role access 196 | - Known vendors with IAM role access 197 | - Unknown AWS accounts with IAM role access 198 | - IAM roles missing ExternalId condition (vulnerable to confused deputy) 199 | - Trusted entities with S3 bucket access 200 | - Known vendors with S3 bucket access 201 | - Unknown AWS accounts with S3 bucket access 202 | 203 | ## Contributing 204 | 205 | Contributions are welcome! If you know of additional AWS account IDs that should be added to the [reference data](https://github.com/fwdcloudsec/known_aws_accounts/), please also contribute to this repository. 206 | 207 | ## License 208 | 209 | This project is licensed under the MIT License. 210 | -------------------------------------------------------------------------------- /check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Know Your Enemies (KYE) - AWS Account Analysis Tool 4 | 5 | This script analyzes IAM Role trust policies and S3 bucket policies in your AWS account 6 | to identify third-party vendors with access to your resources. It compares the AWS account IDs 7 | found in these policies against a reference list of [known AWS accounts from fwd:cloudsec](https://github.com/fwdcloudsec/known_aws_accounts/) to identify 8 | the vendors behind these accounts. 9 | 10 | Usage: 11 | python check.py 12 | """ 13 | 14 | import boto3 15 | import yaml 16 | import json 17 | import requests 18 | import sys 19 | import os 20 | from datetime import datetime 21 | from botocore.exceptions import ClientError 22 | from rich.console import Console 23 | from rich.table import Table 24 | from rich.panel import Panel 25 | from rich import box 26 | 27 | # Initialize rich console for nice output 28 | console = Console() 29 | 30 | 31 | def fetch_reference_data(): 32 | """ 33 | Fetch the reference data of known AWS accounts from GitHub. 34 | 35 | Returns: 36 | dict: Mapping of AWS account IDs to vendor names 37 | """ 38 | try: 39 | # Fetch latest data from GitHub repository 40 | url = "https://raw.githubusercontent.com/fwdcloudsec/known_aws_accounts/main/accounts.yaml" 41 | response = requests.get(url) 42 | response.raise_for_status() 43 | 44 | # Parse YAML content 45 | vendors_data = yaml.safe_load(response.text) 46 | 47 | # Create a mapping of account IDs to vendor names 48 | account_to_vendor = {} 49 | for vendor in vendors_data: 50 | for account_id in vendor.get("accounts", []): 51 | account_to_vendor[account_id] = { 52 | "name": vendor.get("name", "Unknown"), 53 | "type": vendor.get("type", "third-party"), 54 | "source": vendor.get("source", []), 55 | } 56 | 57 | return account_to_vendor 58 | 59 | except Exception as e: 60 | console.print(f"[bold red]Error fetching reference data: {str(e)}[/bold red]") 61 | return {} 62 | 63 | 64 | def fetch_org_accounts(): 65 | """ 66 | Fetch AWS accounts from AWS Organizations API. 67 | 68 | Returns: 69 | tuple: (account_to_internal, error_message) 70 | account_to_internal (dict): Mapping of AWS account IDs to internal names 71 | error_message (str): Error message if any, None if successful 72 | """ 73 | try: 74 | org_client = boto3.client("organizations") 75 | account_to_internal = {} 76 | 77 | # Use paginator to handle large number of accounts 78 | paginator = org_client.get_paginator("list_accounts") 79 | for page in paginator.paginate(): 80 | for account in page["Accounts"]: 81 | account_id = account["Id"] 82 | account_name = account["Name"] 83 | account_to_internal[account_id] = { 84 | "name": account_name, 85 | "type": "trusted", 86 | "description": "AWS Organization Account", 87 | "source": "aws_org", 88 | } 89 | 90 | console.print( 91 | f"[green]✅ Found {len(account_to_internal)} accounts in AWS Organization[/green]" 92 | ) 93 | return account_to_internal, None 94 | 95 | except Exception as e: 96 | error_msg = str(e) 97 | if "AccessDenied" in error_msg or "UnauthorizedOperation" in error_msg: 98 | error_msg = "Access denied to AWS Organizations API. Please ensure you have the required permissions." 99 | console.print( 100 | f"[bold yellow]Warning: Could not fetch AWS Organization accounts: {error_msg}[/bold yellow]" 101 | ) 102 | return {}, error_msg 103 | 104 | 105 | def fetch_trusted_accounts(): 106 | """ 107 | Fetch trusted AWS accounts from both local file and AWS Organizations API. 108 | 109 | Returns: 110 | tuple: (trusted_accounts, org_error) 111 | trusted_accounts (dict): Mapping of trusted AWS account IDs to internal names 112 | org_error (str): Error message from AWS Organizations if any, None if successful 113 | """ 114 | trusted_accounts = {} 115 | 116 | # First try to fetch from AWS Organizations 117 | org_accounts, org_error = fetch_org_accounts() 118 | trusted_accounts.update(org_accounts) 119 | 120 | # Then try to load from YAML file 121 | try: 122 | trusted_accounts_file = "trusted_accounts.yaml" 123 | 124 | if not os.path.exists(trusted_accounts_file): 125 | console.print( 126 | "[yellow]No trusted accounts file found. Using only AWS Organization accounts.[/yellow]" 127 | ) 128 | return trusted_accounts, org_error 129 | 130 | with open(trusted_accounts_file, "r") as file: 131 | trusted_data = yaml.safe_load(file) or [] 132 | 133 | # Create a mapping of account IDs to internal names 134 | for entity in trusted_data: 135 | for account_id in entity.get("accounts", []): 136 | # Only add if not already present from AWS Organizations 137 | if account_id not in trusted_accounts: 138 | trusted_accounts[account_id] = { 139 | "name": entity.get("name", "Internal"), 140 | "type": "trusted", 141 | "description": entity.get("description", ""), 142 | "source": "yaml_file", 143 | } 144 | 145 | console.print( 146 | f"[green]✅ Loaded {len(trusted_accounts) - len(org_accounts)} additional trusted AWS accounts from YAML file[/green]" 147 | ) 148 | return trusted_accounts, org_error 149 | 150 | except Exception as e: 151 | console.print( 152 | f"[bold yellow]Warning: Could not load trusted accounts from YAML file: {str(e)}[/bold yellow]" 153 | ) 154 | return trusted_accounts, org_error 155 | 156 | 157 | def get_account_aliases(): 158 | """ 159 | Get AWS account aliases for all AWS accounts found during analysis. 160 | 161 | Returns: 162 | dict: Mapping of AWS account IDs to their aliases 163 | """ 164 | try: 165 | sts_client = boto3.client("sts") 166 | iam_client = boto3.client("iam") 167 | 168 | # Get current account ID 169 | current_account_id = sts_client.get_caller_identity()["Account"] 170 | 171 | # Get account alias for current account 172 | aliases = {} 173 | try: 174 | response = iam_client.list_account_aliases() 175 | if response["AccountAliases"]: 176 | aliases[current_account_id] = response["AccountAliases"][0] 177 | else: 178 | aliases[current_account_id] = current_account_id 179 | except Exception: 180 | aliases[current_account_id] = current_account_id 181 | 182 | return aliases 183 | 184 | except Exception as e: 185 | console.print( 186 | f"[bold yellow]Warning: Could not get account aliases: {str(e)}[/bold yellow]" 187 | ) 188 | return {} 189 | 190 | 191 | def extract_account_ids_from_policy(policy_document): 192 | """ 193 | Extract AWS account IDs from a policy document. 194 | 195 | Args: 196 | policy_document (dict): The policy document to analyze 197 | 198 | Returns: 199 | list: List of unique AWS account IDs found in the policy 200 | """ 201 | account_ids = set() 202 | 203 | def search_for_accounts(node): 204 | if isinstance(node, dict): 205 | for key, value in node.items(): 206 | if key == "AWS": 207 | if isinstance(value, str) and "arn:aws" in value: 208 | # Extract account ID from ARN 209 | parts = value.split(":") 210 | if len(parts) >= 5: 211 | account_id = parts[4] 212 | if account_id.isdigit() and len(account_id) == 12: 213 | account_ids.add(account_id) 214 | elif ( 215 | isinstance(value, str) and value.isdigit() and len(value) == 12 216 | ): 217 | account_ids.add(value) 218 | elif isinstance(value, list): 219 | for item in value: 220 | if isinstance(item, str) and "arn:aws" in item: 221 | parts = item.split(":") 222 | if len(parts) >= 5: 223 | account_id = parts[4] 224 | if account_id.isdigit() and len(account_id) == 12: 225 | account_ids.add(account_id) 226 | elif ( 227 | isinstance(item, str) 228 | and item.isdigit() 229 | and len(item) == 12 230 | ): 231 | account_ids.add(item) 232 | else: 233 | search_for_accounts(value) 234 | elif isinstance(node, list): 235 | for item in node: 236 | search_for_accounts(item) 237 | 238 | search_for_accounts(policy_document) 239 | return list(account_ids) 240 | 241 | 242 | def check_external_id_condition(policy_document): 243 | """ 244 | Check if a trust policy has ExternalId condition to prevent confused deputy problem. 245 | 246 | Args: 247 | policy_document (dict): The policy document to analyze 248 | 249 | Returns: 250 | bool: True if ExternalId condition exists, False otherwise 251 | """ 252 | if not policy_document or "Statement" not in policy_document: 253 | return False 254 | 255 | # Convert to list if it's a single statement 256 | statements = policy_document["Statement"] 257 | if not isinstance(statements, list): 258 | statements = [statements] 259 | 260 | for statement in statements: 261 | if statement.get("Effect") != "Allow": 262 | continue 263 | 264 | # Check if the statement is for cross-account access 265 | principal = statement.get("Principal", {}) 266 | if not isinstance(principal, dict): 267 | continue 268 | 269 | aws_principal = principal.get("AWS", "") 270 | if not aws_principal: 271 | continue 272 | 273 | # Now check if there's a proper ExternalId condition 274 | condition = statement.get("Condition", {}) 275 | if not condition: 276 | return False 277 | 278 | for condition_type, condition_values in condition.items(): 279 | if condition_type in ["StringEquals", "StringLike", "ArnLike"]: 280 | if "sts:ExternalId" in condition_values: 281 | return True 282 | 283 | return False 284 | 285 | 286 | def check_iam_role_trust_policies(account_to_vendor, trusted_accounts, account_aliases): 287 | """ 288 | Check IAM Role trust policies for known AWS accounts. 289 | 290 | Args: 291 | account_to_vendor (dict): Mapping of AWS account IDs to vendor names 292 | trusted_accounts (dict): Mapping of trusted AWS account IDs to internal names 293 | account_aliases (dict): Mapping of AWS account IDs to their aliases 294 | 295 | Returns: 296 | tuple: (known_vendors, unknown_accounts, trusted_entities, vulnerable_roles) 297 | """ 298 | console.print("[bold blue]Checking IAM role trust policies...[/bold blue]") 299 | 300 | iam_client = boto3.client("iam") 301 | known_vendors = {} 302 | unknown_accounts = {} 303 | trusted_entities = {} 304 | vulnerable_roles = {} 305 | 306 | try: 307 | paginator = iam_client.get_paginator("list_roles") 308 | for page in paginator.paginate(): 309 | for role in page["Roles"]: 310 | role_name = role["RoleName"] 311 | trust_policy = role.get("AssumeRolePolicyDocument", {}) 312 | 313 | # Extract account IDs from the trust policy 314 | account_ids = extract_account_ids_from_policy(trust_policy) 315 | 316 | for account_id in account_ids: 317 | # Skip checking service roles (AWS services) 318 | if account_id == "": 319 | continue 320 | 321 | # Check if account is in trusted zone 322 | if account_id in trusted_accounts: 323 | trusted_name = trusted_accounts[account_id]["name"] 324 | source = trusted_accounts[account_id]["source"] 325 | if trusted_name not in trusted_entities: 326 | trusted_entities[trusted_name] = { 327 | "roles": [], 328 | "source": source, 329 | } 330 | trusted_entities[trusted_name]["roles"].append(role_name) 331 | 332 | # Check for missing ExternalId condition for trusted accounts 333 | has_external_id = check_external_id_condition(trust_policy) 334 | if not has_external_id: 335 | if trusted_name not in vulnerable_roles: 336 | vulnerable_roles[trusted_name] = { 337 | "roles": [], 338 | "source": source, 339 | } 340 | vulnerable_roles[trusted_name]["roles"].append(role_name) 341 | # Check if account is a known vendor 342 | elif account_id in account_to_vendor: 343 | vendor_name = account_to_vendor[account_id]["name"] 344 | if vendor_name not in known_vendors: 345 | known_vendors[vendor_name] = [] 346 | known_vendors[vendor_name].append(role_name) 347 | 348 | # Check for missing ExternalId condition for vendors 349 | has_external_id = check_external_id_condition(trust_policy) 350 | if not has_external_id: 351 | if vendor_name not in vulnerable_roles: 352 | vulnerable_roles[vendor_name] = { 353 | "roles": [], 354 | "source": "vendor", 355 | } 356 | vulnerable_roles[vendor_name]["roles"].append(role_name) 357 | # Add to unknown accounts 358 | else: 359 | # Format account ID with alias if available 360 | display_id = account_id 361 | if account_id in account_aliases: 362 | display_id = f"{account_id} ({account_aliases[account_id]})" 363 | 364 | if display_id not in unknown_accounts: 365 | unknown_accounts[display_id] = [] 366 | unknown_accounts[display_id].append(role_name) 367 | 368 | # Check for missing ExternalId condition for unknown accounts 369 | has_external_id = check_external_id_condition(trust_policy) 370 | if not has_external_id: 371 | if display_id not in vulnerable_roles: 372 | vulnerable_roles[display_id] = { 373 | "roles": [], 374 | "source": "unknown", 375 | } 376 | vulnerable_roles[display_id]["roles"].append(role_name) 377 | 378 | return known_vendors, unknown_accounts, trusted_entities, vulnerable_roles 379 | 380 | except Exception as e: 381 | console.print( 382 | f"[bold red]Error checking IAM role trust policies: {str(e)}[/bold red]" 383 | ) 384 | return {}, {}, {}, {} 385 | 386 | 387 | def check_s3_bucket_policies(account_to_vendor, trusted_accounts, account_aliases): 388 | """ 389 | Check S3 bucket policies for known AWS accounts. 390 | 391 | Args: 392 | account_to_vendor (dict): Mapping of AWS account IDs to vendor names 393 | trusted_accounts (dict): Mapping of trusted AWS account IDs to internal names 394 | account_aliases (dict): Mapping of AWS account IDs to their aliases 395 | 396 | Returns: 397 | tuple: (known_vendors, unknown_accounts, trusted_entities) 398 | """ 399 | console.print("[bold blue]Checking S3 bucket policies...[/bold blue]") 400 | 401 | s3_client = boto3.client("s3") 402 | known_vendors = {} 403 | unknown_accounts = {} 404 | trusted_entities = {} 405 | 406 | try: 407 | response = s3_client.list_buckets() 408 | for bucket in response["Buckets"]: 409 | bucket_name = bucket["Name"] 410 | 411 | try: 412 | # Get bucket policy if it exists 413 | policy_response = s3_client.get_bucket_policy(Bucket=bucket_name) 414 | policy_document = json.loads(policy_response["Policy"]) 415 | 416 | # Extract account IDs from the bucket policy 417 | account_ids = extract_account_ids_from_policy(policy_document) 418 | 419 | for account_id in account_ids: 420 | # Check if account is in trusted zone 421 | if account_id in trusted_accounts: 422 | trusted_name = trusted_accounts[account_id]["name"] 423 | source = trusted_accounts[account_id]["source"] 424 | if trusted_name not in trusted_entities: 425 | trusted_entities[trusted_name] = { 426 | "buckets": [], 427 | "source": source, 428 | } 429 | trusted_entities[trusted_name]["buckets"].append(bucket_name) 430 | # Check if account is a known vendor 431 | elif account_id in account_to_vendor: 432 | vendor_name = account_to_vendor[account_id]["name"] 433 | if vendor_name not in known_vendors: 434 | known_vendors[vendor_name] = [] 435 | known_vendors[vendor_name].append(bucket_name) 436 | # Add to unknown accounts 437 | else: 438 | # Format account ID with alias if available 439 | display_id = account_id 440 | if account_id in account_aliases: 441 | display_id = f"{account_id} ({account_aliases[account_id]})" 442 | 443 | if display_id not in unknown_accounts: 444 | unknown_accounts[display_id] = [] 445 | unknown_accounts[display_id].append(bucket_name) 446 | 447 | except ClientError as e: 448 | # Skip buckets without policies 449 | if e.response["Error"]["Code"] == "NoSuchBucketPolicy": 450 | continue 451 | else: 452 | console.print( 453 | f"[yellow]Warning: Could not check policy for bucket {bucket_name}: {e.response['Error']['Message']}[/yellow]" 454 | ) 455 | 456 | return known_vendors, unknown_accounts, trusted_entities 457 | 458 | except Exception as e: 459 | console.print( 460 | f"[bold red]Error checking S3 bucket policies: {str(e)}[/bold red]" 461 | ) 462 | return {}, {}, {} 463 | 464 | 465 | def generate_report( 466 | iam_known_vendors, 467 | iam_unknown_accounts, 468 | iam_trusted_entities, 469 | iam_vulnerable_roles, 470 | s3_known_vendors, 471 | s3_unknown_accounts, 472 | s3_trusted_entities, 473 | account_aliases, 474 | org_error=None, 475 | ): 476 | """ 477 | Generate a report with the findings. 478 | 479 | Args: 480 | iam_known_vendors (dict): Known vendors found in IAM role trust policies 481 | iam_unknown_accounts (dict): Unknown accounts found in IAM role trust policies 482 | iam_trusted_entities (dict): Trusted entities found in IAM role trust policies 483 | iam_vulnerable_roles (dict): Roles without ExternalId condition 484 | s3_known_vendors (dict): Known vendors found in S3 bucket policies 485 | s3_unknown_accounts (dict): Unknown accounts found in S3 bucket policies 486 | s3_trusted_entities (dict): Trusted entities found in S3 bucket policies 487 | account_aliases (dict): Mapping of AWS account IDs to their aliases 488 | org_error (str): Error message from AWS Organizations if any 489 | """ 490 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 491 | 492 | # Get current account info 493 | current_account_id = ( 494 | list(account_aliases.keys())[0] if account_aliases else "Unknown" 495 | ) 496 | current_account_alias = account_aliases.get(current_account_id, current_account_id) 497 | 498 | report_file = f"aws_account_analysis_{current_account_id}_{timestamp}.md" 499 | 500 | with open(report_file, "w") as f: 501 | f.write("# AWS Account Access Analysis Report\n\n") 502 | f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") 503 | f.write(f"Account: {current_account_id} ({current_account_alias})\n\n") 504 | 505 | # Add AWS Organizations access status 506 | if org_error: 507 | f.write("## ⚠️ AWS Organizations Access\n\n") 508 | f.write(f"Could not access AWS Organizations API: {org_error}\n") 509 | f.write( 510 | "\nThis means the report may be missing trusted accounts from your AWS Organization.\n" 511 | ) 512 | f.write( 513 | "To fix this, ensure your IAM user/role has the `organizations:ListAccounts` permission.\n\n" 514 | ) 515 | 516 | # IAM Roles Section 517 | f.write("# IAM Roles Analysis\n\n") 518 | 519 | # Write IAM trusted entities section 520 | f.write("## Trusted Entities with IAM Role Access\n\n") 521 | if iam_trusted_entities: 522 | f.write("| Entity | Source | IAM Roles |\n") 523 | f.write("|--------|--------|----------|\n") 524 | for entity, data in iam_trusted_entities.items(): 525 | f.write( 526 | f"| {entity} | {data['source']} | {', '.join(data['roles'])} |\n" 527 | ) 528 | else: 529 | f.write("No trusted entities found in IAM role trust policies.\n") 530 | f.write("\n") 531 | 532 | # Write IAM known vendors section 533 | f.write("## Known Vendors with IAM Role Access\n\n") 534 | if iam_known_vendors: 535 | f.write("| Vendor | IAM Roles |\n") 536 | f.write("|--------|----------|\n") 537 | for vendor, roles in iam_known_vendors.items(): 538 | f.write(f"| {vendor} | {', '.join(roles)} |\n") 539 | else: 540 | f.write("No known vendors found in IAM role trust policies.\n") 541 | f.write("\n") 542 | 543 | # Write IAM unknown accounts section 544 | f.write("## Unknown AWS Accounts with IAM Role Access\n\n") 545 | if iam_unknown_accounts: 546 | f.write("| AWS Account ID | Account Name | IAM Roles |\n") 547 | f.write("|---------------|------------|----------|\n") 548 | for account_id, roles in iam_unknown_accounts.items(): 549 | account_name = account_aliases.get(account_id, "Unknown") 550 | f.write(f"| {account_id} | {account_name} | {', '.join(roles)} |\n") 551 | else: 552 | f.write("No unknown AWS accounts found in IAM role trust policies.\n") 553 | f.write("\n") 554 | 555 | # Write vulnerable roles section (missing ExternalId) 556 | f.write("## ⚠️ IAM Roles Missing ExternalId Condition\n\n") 557 | f.write( 558 | "These roles are vulnerable to the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).\n\n" 559 | ) 560 | if iam_vulnerable_roles: 561 | f.write("| Entity | Source | Vulnerable IAM Roles |\n") 562 | f.write("|--------|--------|--------------------|\n") 563 | for entity, data in iam_vulnerable_roles.items(): 564 | f.write( 565 | f"| {entity} | {data['source']} | {', '.join(data['roles'])} |\n" 566 | ) 567 | else: 568 | f.write("No vulnerable IAM roles found (good job!).\n") 569 | f.write("\n") 570 | 571 | # S3 Bucket Policies Section 572 | f.write("# S3 Bucket Policies Analysis\n\n") 573 | 574 | # Write S3 trusted entities section 575 | f.write("## Trusted Entities with S3 Bucket Access\n\n") 576 | if s3_trusted_entities: 577 | f.write("| Entity | Source | S3 Buckets |\n") 578 | f.write("|--------|--------|----------|\n") 579 | for entity, data in s3_trusted_entities.items(): 580 | f.write( 581 | f"| {entity} | {data['source']} | {', '.join(data['buckets'])} |\n" 582 | ) 583 | else: 584 | f.write("No trusted entities found in S3 bucket policies.\n") 585 | f.write("\n") 586 | 587 | # Write S3 known vendors section 588 | f.write("## Known Vendors with S3 Bucket Access\n\n") 589 | if s3_known_vendors: 590 | f.write("| Vendor | S3 Buckets |\n") 591 | f.write("|--------|----------|\n") 592 | for vendor, buckets in s3_known_vendors.items(): 593 | f.write(f"| {vendor} | {', '.join(buckets)} |\n") 594 | else: 595 | f.write("No known vendors found in S3 bucket policies.\n") 596 | f.write("\n") 597 | 598 | # Write S3 unknown accounts section 599 | f.write("## Unknown AWS Accounts with S3 Bucket Access\n\n") 600 | if s3_unknown_accounts: 601 | f.write("| AWS Account ID | Account Name | S3 Buckets |\n") 602 | f.write("|---------------|------------|----------|\n") 603 | for account_id, buckets in s3_unknown_accounts.items(): 604 | account_name = account_aliases.get(account_id, "Unknown") 605 | f.write(f"| {account_id} | {account_name} | {', '.join(buckets)} |\n") 606 | else: 607 | f.write("No unknown AWS accounts found in S3 bucket policies.\n") 608 | 609 | return report_file 610 | 611 | 612 | def display_results( 613 | iam_known_vendors, 614 | iam_unknown_accounts, 615 | iam_trusted_entities, 616 | iam_vulnerable_roles, 617 | s3_known_vendors, 618 | s3_unknown_accounts, 619 | s3_trusted_entities, 620 | account_aliases, 621 | ): 622 | """ 623 | Display the results in a nice format with emojis. 624 | 625 | Args: 626 | iam_known_vendors (dict): Known vendors found in IAM role trust policies 627 | iam_unknown_accounts (dict): Unknown accounts found in IAM role trust policies 628 | iam_trusted_entities (dict): Trusted entities found in IAM role trust policies 629 | iam_vulnerable_roles (dict): Roles without ExternalId condition 630 | s3_known_vendors (dict): Known vendors found in S3 bucket policies 631 | s3_unknown_accounts (dict): Unknown accounts found in S3 bucket policies 632 | s3_trusted_entities (dict): Trusted entities found in S3 bucket policies 633 | account_aliases (dict): Mapping of AWS account IDs to their aliases 634 | """ 635 | # Get current account info 636 | current_account_id = ( 637 | list(account_aliases.keys())[0] if account_aliases else "Unknown" 638 | ) 639 | current_account_alias = account_aliases.get(current_account_id, current_account_id) 640 | 641 | console.print( 642 | f"[cyan]Analyzing AWS Account:[/cyan] {current_account_id} ({current_account_alias})" 643 | ) 644 | 645 | # Display IAM trusted entities table 646 | if iam_trusted_entities: 647 | table = Table(title="🔒 Trusted Entities with IAM Role Access", box=box.ROUNDED) 648 | table.add_column("Entity", style="green") 649 | table.add_column("Source", style="blue") 650 | table.add_column("IAM Roles", style="blue") 651 | 652 | for entity, data in iam_trusted_entities.items(): 653 | table.add_row( 654 | entity, 655 | data["source"], 656 | "\n".join(data["roles"][:5]) 657 | + ("\n..." if len(data["roles"]) > 5 else ""), 658 | ) 659 | 660 | console.print(table) 661 | 662 | # Display IAM known vendors table 663 | if iam_known_vendors: 664 | table = Table(title="✅ Known Vendors with IAM Role Access", box=box.ROUNDED) 665 | table.add_column("Vendor", style="cyan") 666 | table.add_column("IAM Roles", style="green") 667 | 668 | for vendor, roles in iam_known_vendors.items(): 669 | table.add_row( 670 | vendor, "\n".join(roles[:5]) + ("\n..." if len(roles) > 5 else "") 671 | ) 672 | 673 | console.print(table) 674 | 675 | # Display IAM unknown accounts table 676 | if iam_unknown_accounts: 677 | table = Table( 678 | title="❓ Unknown AWS Accounts with IAM Role Access", box=box.ROUNDED 679 | ) 680 | table.add_column("AWS Account ID", style="yellow") 681 | table.add_column("Account Name", style="blue") 682 | table.add_column("IAM Roles", style="green") 683 | 684 | for account_id, roles in iam_unknown_accounts.items(): 685 | # Get account name from AWS Organizations if available 686 | account_name = account_aliases.get(account_id, "Unknown") 687 | table.add_row( 688 | account_id, 689 | account_name, 690 | "\n".join(roles[:5]) + ("\n..." if len(roles) > 5 else ""), 691 | ) 692 | 693 | console.print(table) 694 | 695 | # Display vulnerable roles table (missing ExternalId) 696 | if iam_vulnerable_roles: 697 | table = Table( 698 | title="⚠️ IAM Roles Missing ExternalId Condition (Vulnerable to Confused Deputy)", 699 | box=box.ROUNDED, 700 | ) 701 | table.add_column("Entity", style="red") 702 | table.add_column("Source", style="blue") 703 | table.add_column("Vulnerable IAM Roles", style="red") 704 | 705 | for entity, data in iam_vulnerable_roles.items(): 706 | table.add_row( 707 | entity, 708 | data["source"], 709 | "\n".join(data["roles"][:5]) 710 | + ("\n..." if len(data["roles"]) > 5 else ""), 711 | ) 712 | 713 | console.print(table) 714 | 715 | # Display S3 trusted entities table 716 | if s3_trusted_entities: 717 | table = Table( 718 | title="🔒 Trusted Entities with S3 Bucket Access", box=box.ROUNDED 719 | ) 720 | table.add_column("Entity", style="green") 721 | table.add_column("Source", style="blue") 722 | table.add_column("S3 Buckets", style="blue") 723 | 724 | for entity, data in s3_trusted_entities.items(): 725 | table.add_row( 726 | entity, 727 | data["source"], 728 | "\n".join(data["buckets"][:5]) 729 | + ("\n..." if len(data["buckets"]) > 5 else ""), 730 | ) 731 | 732 | console.print(table) 733 | 734 | # Display S3 known vendors table 735 | if s3_known_vendors: 736 | table = Table(title="✅ Known Vendors with S3 Bucket Access", box=box.ROUNDED) 737 | table.add_column("Vendor", style="cyan") 738 | table.add_column("S3 Buckets", style="green") 739 | 740 | for vendor, buckets in s3_known_vendors.items(): 741 | table.add_row( 742 | vendor, "\n".join(buckets[:5]) + ("\n..." if len(buckets) > 5 else "") 743 | ) 744 | 745 | console.print(table) 746 | 747 | # Display S3 unknown accounts table 748 | if s3_unknown_accounts: 749 | table = Table( 750 | title="❓ Unknown AWS Accounts with S3 Bucket Access", box=box.ROUNDED 751 | ) 752 | table.add_column("AWS Account ID", style="yellow") 753 | table.add_column("Account Name", style="blue") 754 | table.add_column("S3 Buckets", style="green") 755 | 756 | for account_id, buckets in s3_unknown_accounts.items(): 757 | # Get account name from AWS Organizations if available 758 | account_name = account_aliases.get(account_id, "Unknown") 759 | table.add_row( 760 | account_id, 761 | account_name, 762 | "\n".join(buckets[:5]) + ("\n..." if len(buckets) > 5 else ""), 763 | ) 764 | 765 | console.print(table) 766 | 767 | # Display summary 768 | total_trusted = len(iam_trusted_entities) + len(s3_trusted_entities) 769 | total_known = len(iam_known_vendors) + len(s3_known_vendors) 770 | total_unknown = len(iam_unknown_accounts) + len(s3_unknown_accounts) 771 | total_vulnerable = len(iam_vulnerable_roles) 772 | 773 | console.print( 774 | Panel( 775 | f"[bold]Summary:[/bold]\n" 776 | f"🔒 [green]Trusted entities found:[/green] {total_trusted}\n" 777 | f"🔍 [cyan]Known vendors found:[/cyan] {total_known}\n" 778 | f"❓ [yellow]Unknown AWS accounts found:[/yellow] {total_unknown}\n" 779 | f"⚠️ [red]Vulnerable IAM roles (missing ExternalId):[/red] {total_vulnerable}", 780 | title="AWS Account Analysis Results", 781 | box=box.ROUNDED, 782 | ) 783 | ) 784 | 785 | 786 | def main(): 787 | """ 788 | Main function to run the script. 789 | """ 790 | try: 791 | # Display welcome message 792 | console.print( 793 | Panel( 794 | "[bold cyan]Know Your Enemies - AWS Account Analysis Tool[/bold cyan]\n" 795 | "This tool analyzes IAM Role trust policies and S3 bucket policies\n" 796 | "to identify third-party vendors with access to your resources.", 797 | title="🔍 AWS Analysis", 798 | box=box.ROUNDED, 799 | ) 800 | ) 801 | 802 | # Fetch reference data 803 | console.print("[bold]Fetching reference data of known AWS accounts...[/bold]") 804 | account_to_vendor = fetch_reference_data() 805 | console.print( 806 | f"[green]✅ Found {len(account_to_vendor)} known AWS accounts in the reference data[/green]" 807 | ) 808 | 809 | # Fetch trusted accounts 810 | console.print("[bold]Loading trusted AWS accounts...[/bold]") 811 | trusted_accounts, org_error = fetch_trusted_accounts() 812 | 813 | # Get account aliases 814 | account_aliases = get_account_aliases() 815 | 816 | # Check IAM role trust policies 817 | ( 818 | iam_known_vendors, 819 | iam_unknown_accounts, 820 | iam_trusted_entities, 821 | iam_vulnerable_roles, 822 | ) = check_iam_role_trust_policies( 823 | account_to_vendor, trusted_accounts, account_aliases 824 | ) 825 | 826 | # Check S3 bucket policies 827 | s3_known_vendors, s3_unknown_accounts, s3_trusted_entities = ( 828 | check_s3_bucket_policies( 829 | account_to_vendor, trusted_accounts, account_aliases 830 | ) 831 | ) 832 | 833 | # Display results 834 | display_results( 835 | iam_known_vendors, 836 | iam_unknown_accounts, 837 | iam_trusted_entities, 838 | iam_vulnerable_roles, 839 | s3_known_vendors, 840 | s3_unknown_accounts, 841 | s3_trusted_entities, 842 | account_aliases, 843 | ) 844 | 845 | # Generate report 846 | report_file = generate_report( 847 | iam_known_vendors, 848 | iam_unknown_accounts, 849 | iam_trusted_entities, 850 | iam_vulnerable_roles, 851 | s3_known_vendors, 852 | s3_unknown_accounts, 853 | s3_trusted_entities, 854 | account_aliases, 855 | org_error, 856 | ) 857 | console.print(f"[bold green]✅ Report generated: {report_file}[/bold green]") 858 | 859 | except Exception as e: 860 | console.print(f"[bold red]Error: {str(e)}[/bold red]") 861 | return 1 862 | 863 | return 0 864 | 865 | 866 | if __name__ == "__main__": 867 | sys.exit(main()) 868 | --------------------------------------------------------------------------------