From ecd8750e68ac64d08698469582d9942137b755e9 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 2 Jan 2026 15:15:02 -0500 Subject: [PATCH] home region check --- .../assess_check_cloudtrail.py | 12 ++- .../cross-organization/check_cloudtrail.py | 75 +++++++------------ 2 files changed, 37 insertions(+), 50 deletions(-) diff --git a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py index 38435672..30a82b3f 100755 --- a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py @@ -8,7 +8,7 @@ from collections import Counter # --- VERSIONING --- -__version__ = "1.1.4" +__version__ = "1.1.5" def find_latest_file(pattern): files = glob.glob(pattern) @@ -44,7 +44,9 @@ def main(): "s3_bytes": 0, "s3_objects": 0, "s3_buckets": set(), "cw_bytes": 0, "cw_group_arns": set(), "sns_topics": set(), "kms_cmk_count": 0, "sse_s3_count": 0, - "logging_active": 0, "logging_stopped": 0 + "logging_active": 0, "logging_stopped": 0, + "total_shadow_regions": 0, + "total_home_regions": 0 } retention_distribution = Counter() @@ -61,6 +63,9 @@ def main(): for key, val in checks.items(): if key == "account_summary": continue if ":" not in key: continue + + stats["total_home_regions"] += 1 + stats["total_shadow_regions"] += int(val.get("shadow_region_count", 0)) trail_arn = val.get("resource") # Now contains the full ARN @@ -92,6 +97,9 @@ def main(): print(f" Logging Status: {stats['logging_active']} Active | {stats['logging_stopped']} Stopped") print(f" S3 Storage: {s3_gb:.2f} GB | {stats['s3_objects']:,} objects") print(f" CloudWatch Logs: {cw_gb:.2f} GB | {len(stats['cw_group_arns'])} unique groups") + print(f" Primary (Home) Trails: {stats['total_home_regions']}") + print(f" Shadow Regions Covered: {stats['total_shadow_regions']}") + print(f" Total Regional Presence: {stats['total_home_regions'] + stats['total_shadow_regions']}") print("-" * report_width) if __name__ == "__main__": diff --git a/local-app/python-tools/cross-organization/check_cloudtrail.py b/local-app/python-tools/cross-organization/check_cloudtrail.py index e66dcd98..4fb8dc71 100644 --- a/local-app/python-tools/cross-organization/check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/check_cloudtrail.py @@ -3,39 +3,9 @@ from datetime import datetime, timedelta # --- VERSIONING --- -__version__ = "1.1.4" +__version__ = "1.1.6" -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): - if not group_arn: return {} - 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: - return { - "cw_logs_retention_days": g.get('retentionInDays', 'Never Expire'), - "cw_logs_size_bytes": g.get('storedBytes', 0) - } - except: pass - return {} +# ... (get_s3_metrics and get_log_group_details remain same) ... def account_task(account_session, account_id, account_name, region): results = {"alias": "N/A", "data": {}} @@ -45,33 +15,48 @@ def account_task(account_session, account_id, account_name, region): results["alias"] = account_session.client('iam').list_account_aliases().get('AccountAliases', ["N/A"])[0] 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) found_trail_arns = set() for reg in enabled_regions: reg_start = time.perf_counter() ct = account_session.client('cloudtrail', region_name=reg) + + # We must use includeShadowTrails=True to discover trails managed elsewhere trails = ct.describe_trails(includeShadowTrails=True).get('trailList', []) for trail in trails: t_arn = trail['TrailARN'] + home_reg = trail.get('HomeRegion') + is_multi = trail.get('IsMultiRegionTrail', False) + + # Logic: Only process the trail in its Home Region to prevent duplication. + if reg != home_reg: + continue + 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'] + # Calculate Shadow Regions: + # If multi-region, it covers all enabled regions minus the home region. + shadow_count = (total_reg_count - 1) if is_multi else 0 + t_data = { - "resource": t_arn, # Use ARN as the resource for CSV mapping - "trail_name": t_name, # Preserved name in JSON - "trail_arn": t_arn, # New explicit ARN field for JSON - "home_region": trail.get('HomeRegion', reg), + "resource": t_arn, + "trail_name": trail['Name'], + "trail_arn": t_arn, + "home_region": home_reg, + "shadow_region_count": shadow_count, # New field "is_logging": str(status.get('IsLogging', False)), "is_org_trail": str(is_org), - "is_multi_region": str(trail.get('IsMultiRegionTrail', False)), + "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'), @@ -85,18 +70,12 @@ def account_task(account_session, account_id, account_name, region): if t_data['s3_bucket'] != 'N/A': t_data.update(get_s3_metrics(account_session, t_data['s3_bucket'], reg)) - # Key remains Region:ARN to handle multi-trail scenarios results["data"][f"{reg}:{t_arn}"] = t_data - summary = "OK" - if org_trail_count > 0 and local_trail_count > 0: - summary = f"DUPLICATIVE/{local_trail_count}_LOCAL_WITH_ORG" - elif org_trail_count == 0 and local_trail_count > 0: - summary = f"LOCAL_ONLY/{local_trail_count}" - elif org_trail_count == 0 and local_trail_count == 0: - summary = "NO_TRAILS" - - results["data"]["account_summary"] = {"_summary": summary} + results["data"]["account_summary"] = { + "_summary": f"ORG:{org_trail_count}|LOCAL:{local_trail_count}", + "enabled_regions": total_reg_count + } except Exception as e: results["error"] = str(e)