diff --git a/local-app/python-tools/cross-organization/check_cloudtrail.py b/local-app/python-tools/cross-organization/check_cloudtrail.py index 93b554b3..1eff5810 100644 --- a/local-app/python-tools/cross-organization/check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/check_cloudtrail.py @@ -3,7 +3,41 @@ from datetime import datetime, timedelta # --- VERSIONING --- -__version__ = "1.1.7" +__version__ = "1.1.8" + +def get_s3_metrics(session, bucket_name, region): + cw = session.client('cloudwatch', region_name=region) + metrics = {"bucket_size_bytes": 0, "object_count": 0} + end = datetime.utcnow() + start = end - timedelta(days=2) + try: + 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): + """Restored: Fetches CloudWatch Log Group retention and size.""" + if not group_arn: return {} + try: + # Extract name from ARN (arn:aws:logs:region:account:log-group:name) + group_name = group_arn.split(':')[-1] + logs = session.client('logs', region_name=region) + resp = logs.describe_log_groups(logGroupNamePrefix=group_name) + for g in resp.get('logGroups', []): + if g['logGroupName'] == group_name: + return { + "cw_logs_retention_days": g.get('retentionInDays', 'Never Expire'), + "cw_logs_size_bytes": g.get('storedBytes', 0) + } + except: pass + return {} def account_task(account_session, account_id, account_name, region): results = {"alias": "N/A", "data": {}} @@ -14,60 +48,52 @@ def account_task(account_session, account_id, account_name, region): ec2 = account_session.client('ec2', region_name=region) enabled_regions = [r['RegionName'] for r in ec2.describe_regions()['Regions']] total_reg_count = len(enabled_regions) - - # Track ARNs globally across the account scan to prevent duplicates seen_arns = set() for reg in enabled_regions: - # Always ensure the region key exists in the JSON so it's not "empty" results["data"][reg] = {} - ct = account_session.client('cloudtrail', region_name=reg) try: - # Include shadow trails to see multi-region/org trails from any region trails = ct.describe_trails(includeShadowTrails=True).get('trailList', []) - except: - continue # Skip regions where CloudTrail might be restricted + except: continue for trail in trails: t_arn = trail['TrailARN'] - - # DUPLICATION PROTECTION: - # Only process the trail once. If it's multi-region, we'll - # likely see it in the first region we scan. - if t_arn in seen_arns: - continue + if t_arn in seen_arns: continue seen_arns.add(t_arn) - home_reg = trail.get('HomeRegion') - is_multi = trail.get('IsMultiRegionTrail', False) status = ct.get_trail_status(Name=t_arn) is_org = trail.get('IsOrganizationTrail', False) + is_multi = trail.get('IsMultiRegionTrail', False) if is_org: org_trail_count += 1 else: local_trail_count += 1 - shadow_count = (total_reg_count - 1) if is_multi else 0 - t_data = { "resource": t_arn, "trail_name": trail['Name'], "trail_arn": t_arn, - "home_region": home_reg, - "shadow_region_count": shadow_count, + "home_region": trail.get('HomeRegion'), + "shadow_region_count": (total_reg_count - 1) if is_multi else 0, "is_logging": str(status.get('IsLogging', False)), "is_org_trail": str(is_org), "is_multi_region": str(is_multi), "s3_bucket": trail.get('S3BucketName', 'N/A'), "log_file_validation": str(trail.get('LogFileValidationEnabled', False)), "sns_topic": trail.get('SnsTopicARN', 'N/A'), - "kms_key_id": trail.get('KmsKeyId', 'SSE-S3') + "kms_key_id": trail.get('KmsKeyId', 'SSE-S3'), + "cw_logs_arn": trail.get('CloudWatchLogsLogGroupArn', 'N/A') # Restored Field } - # Place the trail data into the region it was discovered in + # Retrieve restored CloudWatch details + if t_data["cw_logs_arn"] != 'N/A': + t_data.update(get_log_group_details(account_session, t_data["cw_logs_arn"], reg)) + + if t_data['s3_bucket'] != 'N/A': + t_data.update(get_s3_metrics(account_session, t_data['s3_bucket'], reg)) + results["data"][f"{reg}:{t_arn}"] = t_data - # Final account summary ensures the file is never "empty" results["data"]["account_summary"] = { "_summary": f"ORG:{org_trail_count}|LOCAL:{local_trail_count}", "enabled_regions": total_reg_count,