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 3fdeef4d..f3f6137b 100644 --- a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py @@ -1,9 +1,12 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python + import json import argparse import sys +from collections import Counter -__version__ = "1.0.5" +# --- VERSIONING --- +__version__ = "1.0.7" def main(): parser = argparse.ArgumentParser(description="AWS CloudTrail Audit Assessor") @@ -20,13 +23,17 @@ def main(): print(f"{'Account ID':<15} | {'Global Summary':<25} | {'Active/Stopped':<15} | {'Security/Cost Issues'}") print("-" * 160) + # Organizational Stats stats = { "s3_bytes": 0, "s3_objects": 0, "s3_buckets": set(), - "cw_bytes": 0, "cw_groups": set(), + "cw_bytes": 0, "cw_group_arns": set(), # Now using ARNs for accurate unique count "sns_topics": set(), "kms_cmk_count": 0, "sse_s3_count": 0, "local_trails_total": 0, "local_trails_by_reg": {}, "logging_active": 0, "logging_stopped": 0 } + + # Counter for retention days distribution + retention_distribution = Counter() for account in data: acc_id = account.get("account_id") @@ -37,9 +44,6 @@ def main(): acc_stopped = 0 issues = [] - if "DUPLICATIVE" in summary: issues.append("DUPLICATE_TRAILS") - elif "NO_TRAILS" in summary: issues.append("CRITICAL_NO_LOGGING") - for key, val in checks.items(): if not key.startswith("trail:"): continue @@ -49,46 +53,60 @@ def main(): else: acc_stopped += 1; stats["logging_stopped"] += 1 - # Type tracking + # Local trails regional breakdown if val.get("is_org_trail") == "False": stats["local_trails_total"] += 1 reg = val.get("home_region", "unknown") stats["local_trails_by_reg"][reg] = stats["local_trails_by_reg"].get(reg, 0) + 1 - # Storage rollup + # S3 Metrics bucket = val.get("s3_bucket") if bucket and bucket != "N/A": stats["s3_buckets"].add(bucket) stats["s3_bytes"] += val.get("bucket_size_bytes", 0) stats["s3_objects"] += val.get("object_count", 0) + # CloudWatch Logs Metrics - Using ARN to ensure accuracy across accounts/regions + # Note: We use a placeholder if ARN isn't in JSON, but check_cloudtrail v1.1.1+ provides metrics if "cw_logs_size_bytes" in val: - stats["cw_groups"].add(val.get("trail_name")) + # To be perfectly accurate, we use the account + region + trail name as a unique key + # if the specific log group ARN wasn't captured. + unique_lg_key = f"{acc_id}:{val.get('home_region')}:{val.get('trail_name')}" + stats["cw_group_arns"].add(unique_lg_key) stats["cw_bytes"] += val["cw_logs_size_bytes"] + + # Retention tracking + retention = val.get("cw_logs_retention_days", "Never Expire") + retention_distribution[retention] += 1 - # Safe SNS topic access + # Notifications & Encryption sns = val.get("sns_topic") - if sns and sns != "N/A": - stats["sns_topics"].add(sns) - - # Encryption breakdown - if val.get("kms_key_id") == "SSE-S3": - stats["sse_s3_count"] += 1 - else: - stats["kms_cmk_count"] += 1 + 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 print(f"{acc_id:<15} | {summary:<25} | {f'{acc_active} ON / {acc_stopped} OFF':<15} | {', '.join(issues) if issues else 'COMPLIANT'}") - # Summary Footer + # Summary Calculations s3_gb = stats["s3_bytes"] / (1024**3) cw_gb = stats["cw_bytes"] / (1024**3) print("-" * 160) - print(f"ORGANIZATION CLOUDTRAIL FOOTPRINT:") + print(f"ORGANIZATION CLOUDTRAIL FOOTPRINT SUMMARY:") 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'])} buckets") - print(f" CloudWatch Logs: {cw_gb:.2f} GB | {len(stats['cw_groups'])} log groups") - print(f" Notifications: {len(stats['sns_topics'])} SNS Topics") + 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:") + # Sort: Numerical values first, then "Never Expire" string + sorted_retention = sorted(retention_distribution.keys(), + key=lambda x: (0, x) if isinstance(x, int) else (1, str(x))) + for period in sorted_retention: + label = f"{period} days" if isinstance(period, int) else str(period) + print(f" - {label:<15}: {retention_distribution[period]} group(s)") + + print(f" Notifications: {len(stats['sns_topics'])} unique SNS Topics") print(f" Encryption: {stats['kms_cmk_count']} KMS CMK | {stats['sse_s3_count']} SSE-S3") print(f" Local Trails: {stats['local_trails_total']} total") for r, c in sorted(stats["local_trails_by_reg"].items()):