diff --git a/local-app/python-tools/cross-organization/check_cloudtrail.py b/local-app/python-tools/cross-organization/check_cloudtrail.py index 812bd4e2..4e47fed1 100644 --- a/local-app/python-tools/cross-organization/check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/check_cloudtrail.py @@ -2,37 +2,30 @@ import time from datetime import datetime, timedelta -__version__ = "1.0.0" +__version__ = "1.1.0" def get_s3_metrics(session, bucket_name, region): - """Fetches bucket storage metrics from CloudWatch.""" cw = session.client('cloudwatch', region_name=region) metrics = {"bucket_size_bytes": 0, "object_count": 0} end = datetime.utcnow() start = end - timedelta(days=2) try: - r1 = cw.get_metric_statistics( - Namespace='AWS/S3', MetricName='BucketSizeBytes', - Dimensions=[{'Name': 'BucketName', 'Value': bucket_name}, {'Name': 'StorageType', 'Value': 'StandardStorage'}], - StartTime=start, EndTime=end, Period=86400, Statistics=['Average'] - ) - if r1['Datapoints']: metrics["bucket_size_bytes"] = int(r1['Datapoints'][-1]['Average']) - - r2 = cw.get_metric_statistics( - Namespace='AWS/S3', MetricName='NumberOfObjects', - Dimensions=[{'Name': 'BucketName', 'Value': bucket_name}, {'Name': 'StorageType', 'Value': 'AllStorageTypes'}], - StartTime=start, EndTime=end, Period=86400, Statistics=['Average'] - ) - if r2['Datapoints']: metrics["object_count"] = int(r2['Datapoints'][-1]['Average']) + # Check both Standard and All Storage types for full visibility + for metric, key in [('BucketSizeBytes', 'bucket_size_bytes'), ('NumberOfObjects', 'object_count')]: + r = cw.get_metric_statistics( + Namespace='AWS/S3', MetricName=metric, + Dimensions=[{'Name': 'BucketName', 'Value': bucket_name}, {'Name': 'StorageType', 'Value': 'StandardStorage' if key == 'bucket_size_bytes' else 'AllStorageTypes'}], + StartTime=start, EndTime=end, Period=86400, Statistics=['Average'] + ) + if r['Datapoints']: metrics[key] = int(r['Datapoints'][-1]['Average']) except: pass return metrics def get_log_group_details(session, group_arn, region): - """Fetches retention and size for CloudWatch Log Groups.""" if not group_arn: return {} - logs = session.client('logs', region_name=region) - group_name = group_arn.split(':')[-2] # Extract name from ARN try: + logs = session.client('logs', region_name=region) + group_name = group_arn.split(':')[-2] if ':' in group_arn else group_arn resp = logs.describe_log_groups(logGroupNamePrefix=group_name) for g in resp.get('logGroups', []): if g['logGroupName'] == group_name: @@ -50,42 +43,49 @@ def account_task(account_session, account_id, account_name, region): try: results["alias"] = account_session.client('iam').list_account_aliases().get('AccountAliases', ["N/A"])[0] - ct = account_session.client('cloudtrail', region_name=region) - - # describe_trails retrieves trails for the current region (+ shadow trails if multi-region) - trails = ct.describe_trails(includeShadowTrails=True).get('trailList', []) + ec2 = account_session.client('ec2', region_name=region) + enabled_regions = [r['RegionName'] for r in ec2.describe_regions()['Regions']] - for trail in trails: - trail_name = trail['Name'] - status = ct.get_trail_status(Name=trail_name) - - is_org = trail.get('IsOrganizationTrail', False) - if is_org: org_trail_count += 1 - else: local_trail_count += 1 - - trail_data = { - "trail_name": trail_name, - "is_logging": str(status.get('IsLogging', False)), - "is_org_trail": str(is_org), - "is_multi_region": str(trail.get('IsMultiRegionTrail', False)), - "log_file_validation": str(trail.get('LogFileValidationEnabled', False)), - "s3_bucket": trail.get('S3BucketName', 'N/A'), - "kms_key_id": trail.get('KmsKeyId', 'SSE-S3 (Default)'), - "sns_topic": trail.get('SNSTopicName', 'N/A'), - "home_region": trail.get('HomeRegion', 'N/A') - } - - # Fetch CloudWatch Logs Integration - if 'CloudWatchLogsLogGroupArn' in trail: - trail_data.update(get_log_group_details(account_session, trail['CloudWatchLogsLogGroupArn'], region)) - - # Fetch S3 Metrics (if bucket is in the same account) - if trail_data['s3_bucket'] != 'N/A': - trail_data.update(get_s3_metrics(account_session, trail_data['s3_bucket'], region)) + found_trail_arns = set() + + for reg in enabled_regions: + ct = account_session.client('cloudtrail', region_name=reg) + # describe_trails with IncludeShadowTrails=True is powerful but we loop to catch + # trails that might be single-region in non-standard regions. + trails = ct.describe_trails(includeShadowTrails=True).get('trailList', []) - results["data"][f"trail:{trail_name}"] = trail_data + for trail in trails: + t_arn = trail['TrailARN'] + if t_arn in found_trail_arns: continue + found_trail_arns.add(t_arn) + + status = ct.get_trail_status(Name=t_arn) + is_org = trail.get('IsOrganizationTrail', False) + if is_org: org_trail_count += 1 + else: local_trail_count += 1 + + t_name = trail['Name'] + t_data = { + "trail_name": t_name, + "home_region": trail.get('HomeRegion', reg), + "is_logging": str(status.get('IsLogging', False)), + "is_org_trail": str(is_org), + "is_multi_region": str(trail.get('IsMultiRegionTrail', False)), + "s3_bucket": trail.get('S3BucketName', 'N/A'), + "log_file_validation": str(trail.get('LogFileValidationEnabled', False)), + "kms_key_id": trail.get('KmsKeyId', 'SSE-S3'), + "check_elapsed_sec": 0 # Placeholder for loop timing if needed + } + + if 'CloudWatchLogsLogGroupArn' in trail: + t_data.update(get_log_group_details(account_session, trail['CloudWatchLogsLogGroupArn'], reg)) + + if t_data['s3_bucket'] != 'N/A': + t_data.update(get_s3_metrics(account_session, t_data['s3_bucket'], reg)) + + results["data"][f"trail:{t_name}:{t_data['home_region']}"] = t_data - # Add Per-Account Summary + # Summary Logic summary = "OK" if org_trail_count > 0 and local_trail_count > 0: summary = f"DUPLICATIVE/{local_trail_count}_LOCAL_WITH_ORG"