From cd10d19c971bdf0140cf185d5cd324cad47f4d99 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 2 Jan 2026 13:30:46 -0500 Subject: [PATCH] use arn, not trail name --- .../assess_check_cloudtrail.py | 56 ++++++++----------- .../cross-organization/check_cloudtrail.py | 11 ++-- 2 files changed, 30 insertions(+), 37 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 ad16087a..38435672 100755 --- a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py @@ -8,14 +8,14 @@ from collections import Counter # --- VERSIONING --- -__version__ = "1.1.3" +__version__ = "1.1.4" def find_latest_file(pattern): files = glob.glob(pattern) return max(files, key=os.path.getctime) if files else None def main(): - parser = argparse.ArgumentParser(description="AWS CloudTrail Audit Assessor") + parser = argparse.ArgumentParser(description="AWS CloudTrail Audit Assessor - ARN Edition") parser.add_argument("--input", help="JSON file (default: latest audit_results.check_cloudtrail.*.json)") args = parser.parse_args() @@ -31,11 +31,14 @@ def main(): org_id = data[0].get("org_id", "Unknown") if data else "Unknown" - print("-" * 180) - print(f"CLOUDTRAIL ASSESSMENT REPORT | Org ID: {org_id} | Input: {os.path.basename(input_file)}") - print("-" * 180) - print(f"{'Account ID':<15} | {'OU Path':<30} | {'Global Summary':<25} | {'Active/Stopped':<15} | {'Security Issues'}") - print("-" * 180) + # EXPANDED COLUMN WIDTHS + # Account(15) | OU(30) | Global(25) | Active(15) | Resource(100) + report_width = 230 + print("-" * report_width) + print(f"CLOUDTRAIL ARN ASSESSMENT REPORT | Org ID: {org_id} | Input: {os.path.basename(input_file)}") + print("-" * report_width) + print(f"{'Account ID':<15} | {'OU Path':<30} | {'Global Summary':<25} | {'Active/Stopped':<15} | {'Resource (ARN)'}") + print("-" * report_width) stats = { "s3_bytes": 0, "s3_objects": 0, "s3_buckets": set(), @@ -53,21 +56,20 @@ def main(): summary = checks.get("account_summary", {}).get("_summary", "UNKNOWN") acc_active, acc_stopped = 0, 0 - issues = [] + # Since one account can have multiple trails, we iterate them for key, val in checks.items(): if key == "account_summary": continue - - # Identify trails by the presence of a colon in the key (Region:TrailName) if ":" not in key: continue - trail_name = val.get("resource") # Extraction from new field + trail_arn = val.get("resource") # Now contains the full ARN if val.get("is_logging") == "True": acc_active += 1; stats["logging_active"] += 1 else: acc_stopped += 1; stats["logging_stopped"] += 1 + # Metric Aggregation bucket = val.get("s3_bucket") if bucket and bucket != "N/A": stats["s3_buckets"].add(bucket) @@ -75,32 +77,22 @@ def main(): stats["s3_objects"] += val.get("object_count", 0) if "cw_logs_size_bytes" in val: - unique_lg_key = f"{acc_id}:{val.get('home_region')}:{trail_name}" - stats["cw_group_arns"].add(unique_lg_key) + stats["cw_group_arns"].add(f"{acc_id}:{trail_arn}") stats["cw_bytes"] += val["cw_logs_size_bytes"] - retention = val.get("cw_logs_retention_days", "Never Expire") - retention_distribution[retention] += 1 - - sns = val.get("sns_topic") - if sns and sns != "N/A": stats["sns_topics"].add(sns) - if val.get("kms_key_id") == "SSE-S3": stats["sse_s3_count"] += 1 - else: stats["kms_cmk_count"] += 1 + retention_distribution[val.get("cw_logs_retention_days", "Never Expire")] += 1 - print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<25} | {f'{acc_active} ON / {acc_stopped} OFF':<15} | COMPLIANT") + # Print row for each trail found + print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<25} | {val.get('is_logging'):<15} | {trail_arn:<100}") + # FOOTPRINT SUMMARY s3_gb, cw_gb = stats["s3_bytes"] / (1024**3), stats["cw_bytes"] / (1024**3) - print("-" * 180) - print(f"ORGANIZATION CLOUDTRAIL FOOTPRINT SUMMARY | Org ID: {org_id}") - print(f" Logging Status: {stats['logging_active']} Active Trails | {stats['logging_stopped']} Stopped Trails") - print(f" S3 Storage: {s3_gb:.2f} GB | {stats['s3_objects']:,} objects | {len(stats['s3_buckets'])} unique buckets") - print(f" CloudWatch Logs: {cw_gb:.2f} GB | {len(stats['cw_group_arns'])} unique log groups") - print(f" Log Group Retention Distribution:") - sorted_ret = sorted(retention_distribution.keys(), key=lambda x: (0, x) if isinstance(x, int) else (1, str(x))) - for period in sorted_ret: - label = f"{period} days" if isinstance(period, int) else str(period) - print(f" - {label:<15}: {retention_distribution[period]} group(s)") - print("-" * 180) + print("-" * report_width) + print(f"ORGANIZATION FOOTPRINT SUMMARY | Org ID: {org_id}") + 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("-" * report_width) if __name__ == "__main__": main() diff --git a/local-app/python-tools/cross-organization/check_cloudtrail.py b/local-app/python-tools/cross-organization/check_cloudtrail.py index eeefdf5c..e66dcd98 100644 --- a/local-app/python-tools/cross-organization/check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/check_cloudtrail.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta # --- VERSIONING --- -__version__ = "1.1.3" +__version__ = "1.1.4" def get_s3_metrics(session, bucket_name, region): cw = session.client('cloudwatch', region_name=region) @@ -51,7 +51,6 @@ def account_task(account_session, account_id, account_name, region): for reg in enabled_regions: reg_start = time.perf_counter() ct = account_session.client('cloudtrail', region_name=reg) - # includeShadowTrails ensures we see trails from other home regions trails = ct.describe_trails(includeShadowTrails=True).get('trailList', []) for trail in trails: @@ -66,7 +65,9 @@ def account_task(account_session, account_id, account_name, region): t_name = trail['Name'] t_data = { - "resource": t_name, # Separated Resource Name + "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), "is_logging": str(status.get('IsLogging', False)), "is_org_trail": str(is_org), @@ -84,8 +85,8 @@ 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)) - # New Key Format: Region:Resource - results["data"][f"{reg}:{t_name}"] = t_data + # 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: