├── .gitignore ├── README.md ├── cloudtrail2IAM.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | launch.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudtrail2IAM 2 | 3 | **CloudTrail2IAM** is a Python tool that analyzes **AWS CloudTrail logs to extract and summarize actions** done by everyone or just an specific user or role. The tool will **parse every cloudtrail log from the indicated bucket**. 4 | 5 | This is useful for red teamers that have **read access over Cloudtrail logs and wants to have more info about the permissions** of the roles and users but **doesn't have read access over IAM**. 6 | 7 | *Note that a Cloudtrail bucket might contain hundreds of thousands of log files. So this could take several minutes/hours.* 8 | 9 | ## Installation 10 | 11 | ```sh 12 | git clone https://github.com/carlospolop/Cloudtrail2IAM 13 | cd Cloudtrail2IAM 14 | pip install -r requirements.txt 15 | python3 cloudtrail2IAM.py --prefix PREFIX --bucket-name BUCKET_NAME --profile PROFILE [--filter-name FILTER_NAME] [--threads THREADS] 16 | ``` 17 | -------------------------------------------------------------------------------- /cloudtrail2IAM.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | import gzip 4 | import json 5 | import tempfile 6 | from tqdm import tqdm 7 | from typing import List, Dict, Any 8 | from concurrent.futures import ThreadPoolExecutor, as_completed 9 | from threading import Semaphore 10 | 11 | ALL_ACTIONS: Dict[str, Dict] = {} 12 | semaphore = Semaphore() 13 | 14 | def get_s3_client(profile: str): 15 | """Create an S3 client using the specified profile.""" 16 | session = boto3.Session(profile_name=profile) 17 | return session.client('s3') 18 | 19 | def download_log_file(s3_client, bucket: str, key: str, local_path: str): 20 | """Download the log file from the specified S3 bucket and key to a local path.""" 21 | s3_client.download_file(bucket, key, local_path) 22 | 23 | def fix_arn(arn: str) -> str: 24 | """Fix the ARN to remove the role name from the path.""" 25 | if arn.startswith('arn:aws:sts::'): 26 | arn = arn.replace('arn:aws:sts::', 'arn:aws:iam::') 27 | arn = arn.replace('assumed-role', 'role') 28 | arn = "/".join(arn.split("/")[:2]) 29 | 30 | return arn 31 | 32 | 33 | def extract_actions_from_log_file(file_path: str): 34 | """Extract actions performed by the target role or user from the downloaded log file.""" 35 | global ALL_ACTIONS 36 | global semaphore 37 | 38 | with gzip.open(file_path, 'rt', encoding='utf-8') as file: 39 | data = json.load(file) 40 | for record in data.get('Records', []): 41 | if 'userIdentity' in record and 'arn' in record['userIdentity']: 42 | arn = fix_arn(record['userIdentity']['arn']) 43 | action = record['eventSource'].split(".")[0] + ":" + record['eventName'] 44 | timestamp = record['eventTime'] 45 | 46 | with semaphore: 47 | if not ALL_ACTIONS.get(arn): 48 | ALL_ACTIONS[arn] = {action: timestamp} 49 | else: 50 | if action in ALL_ACTIONS[arn]: 51 | stored_timestamp = ALL_ACTIONS[arn][action] 52 | if timestamp > stored_timestamp: 53 | ALL_ACTIONS[arn][action] = timestamp 54 | else: 55 | ALL_ACTIONS[arn][action] = timestamp 56 | 57 | def get_all_keys(s3_client, bucket_name: str, prefix: str) -> List[Dict[str, Any]]: 58 | """List all objects with the specified prefix in the S3 bucket.""" 59 | logs_list = [] 60 | paginator = s3_client.get_paginator('list_objects_v2') 61 | 62 | # The max is 1000... 63 | for page in paginator.paginate(Bucket=bucket_name, Prefix=prefix, MaxKeys=1000): 64 | if 'Contents' in page: 65 | logs_list.extend(page['Contents']) 66 | print("Found {} logs".format(len(logs_list)), end='\r') 67 | 68 | print() 69 | return logs_list 70 | 71 | def process_log_object(s3_client, bucket_name: str, log_obj: Dict[str, Any]): 72 | """Download and process each log file.""" 73 | log_key = log_obj['Key'] 74 | if ".json.gz" not in log_key or "digest" in log_key.lower(): 75 | return 76 | 77 | with tempfile.NamedTemporaryFile() as temp_file: 78 | download_log_file(s3_client, bucket_name, log_key, temp_file.name) 79 | extract_actions_from_log_file(temp_file.name) 80 | 81 | def main(): 82 | global ALL_ACTIONS 83 | 84 | parser = argparse.ArgumentParser(description='Analyze CloudTrail logs for specific actions') 85 | parser.add_argument('--prefix', required=True, help='The S3 prefix for CloudTrail logs') 86 | parser.add_argument('--bucket-name', required=True, help='The S3 bucket name containing CloudTrail logs. e.q.: "AWSLogs//CloudTrail/"') 87 | parser.add_argument('--profile', required=True, help='The AWS profile to use for accessing the S3 bucket') 88 | parser.add_argument('--threads', type=int, default=20, help='The number of threads to use for processing log files (default: 20)') 89 | parser.add_argument('--filter-name', required=False, help='Only get actions performed by this name') 90 | args = parser.parse_args() 91 | 92 | target_name = args.filter_name 93 | 94 | s3_client = get_s3_client(args.profile) 95 | 96 | logs_list = get_all_keys(s3_client, args.bucket_name, args.prefix) 97 | 98 | # Use ThreadPoolExecutor to process log files with the specified number of threads 99 | with ThreadPoolExecutor(max_workers=args.threads) as executor: 100 | futures = [executor.submit(process_log_object, s3_client, args.bucket_name, log_obj) for log_obj in logs_list] 101 | for future in tqdm(as_completed(futures), total=len(futures), desc="Cloudtrail files"): 102 | future.result() 103 | 104 | # Fiter by name if specified 105 | if target_name: 106 | all_actions_temp = {} 107 | for arn, actions in ALL_ACTIONS.items(): 108 | if target_name in arn: 109 | all_actions_temp[arn] = actions 110 | ALL_ACTIONS = all_actions_temp 111 | 112 | for arn, actions in ALL_ACTIONS.items(): 113 | print(f'Actions performed by {arn}') 114 | sorted_actions = {key: actions[key] for key in sorted(actions)} 115 | for action,time in sorted_actions.items(): 116 | print(f"- {action} ({time})") 117 | 118 | if __name__ == '__main__': 119 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | tqdm --------------------------------------------------------------------------------