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 568ee6d5..0986e27e 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.5" +__version__ = "1.1.6" 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 - Storage & Retention Edition") parser.add_argument("--input", help="JSON file (default: latest audit_results.check_cloudtrail.*.json)") args = parser.parse_args() @@ -34,22 +34,26 @@ def main(): report_width = 240 print("-" * report_width) - print(f"CLOUDTRAIL ARN ASSESSMENT REPORT | Org ID: {org_id} | Input: {os.path.basename(input_file)}") + print(f"CLOUDTRAIL COMPREHENSIVE ASSESSMENT | Org ID: {org_id} | Input: {os.path.basename(input_file)}") print("-" * report_width) - # Added Idx Column to Header print(f"{'Idx':<5} | {'Account ID':<15} | {'OU Path':<35} | {'Global Summary':<25} | {'Active/Stopped':<15} | {'Resource (ARN)'}") print("-" * report_width) + # UPDATED STATS DICTIONARY stats = { - "s3_bytes": 0, "s3_objects": 0, "s3_buckets": set(), - "cw_bytes": 0, "cw_group_arns": set(), - "logging_active": 0, "logging_stopped": 0, - "total_shadow_regions": 0, "total_home_regions": 0 + "s3_bytes": 0, + "s3_objects": 0, + "s3_buckets": set(), + "cw_bytes": 0, + "cw_log_groups": set(), # Track unique log groups + "logging_active": 0, + "logging_stopped": 0, + "total_shadow_regions": 0, + "total_home_regions": 0 } retention_distribution = Counter() - # Added enumerate to track Index for idx, account in enumerate(data, 1): acc_id = account.get("account_id") ou_path = account.get("ou_path", "Root") @@ -57,40 +61,58 @@ def main(): summary = checks.get("account_summary", {}).get("_summary", "UNKNOWN") for key, val in checks.items(): - if key == "account_summary": continue - if ":" not in key: continue + if key == "account_summary" or ":" not in key: continue trail_arn = val.get("resource") stats["total_home_regions"] += 1 stats["total_shadow_regions"] += int(val.get("shadow_region_count", 0)) + # Status tracking if val.get("is_logging") == "True": stats["logging_active"] += 1 else: stats["logging_stopped"] += 1 + # S3 Aggregation 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) - if "cw_logs_size_bytes" in val: - stats["cw_group_arns"].add(f"{acc_id}:{trail_arn}") - stats["cw_bytes"] += val["cw_logs_size_bytes"] - retention_distribution[val.get("cw_logs_retention_days", "Never Expire")] += 1 + # RESTORED: CloudWatch Aggregation + cw_arn = val.get("cw_logs_arn", "N/A") + if cw_arn != "N/A": + # Key log group by account:region:arn to ensure uniqueness + unique_lg_id = f"{acc_id}:{val.get('home_region')}:{cw_arn}" + stats["cw_log_groups"].add(unique_lg_id) + stats["cw_bytes"] += val.get("cw_logs_size_bytes", 0) + + # Distribution of retention policies + retention = val.get("cw_logs_retention_days", "Never Expire") + retention_distribution[retention] += 1 - # Print row with Index print(f"{idx:<5} | {acc_id:<15} | {ou_path[:35]:<35} | {summary:<25} | {val.get('is_logging'):<15} | {trail_arn:<100}") - s3_gb, cw_gb = stats["s3_bytes"] / (1024**3), stats["cw_bytes"] / (1024**3) + # CONVERT BYTES TO GB + s3_gb = stats["s3_bytes"] / (1024**3) + cw_gb = stats["cw_bytes"] / (1024**3) + # FOOTER SUMMARY print("-" * report_width) print(f"ORGANIZATION CLOUDTRAIL FOOTPRINT SUMMARY | Org ID: {org_id}") - print(f" Total Accounts Found: {account_count}") # Added Account Count - print(f" Primary (Home) Trails: {stats['total_home_regions']}") - print(f" Shadow Regions Covered: {stats['total_shadow_regions']}") - print(f" S3 Storage Total: {s3_gb:.2f} GB") + print(f" Total Accounts Found: {account_count}") + print(f" Logging Status: {stats['logging_active']} Active Trails | {stats['logging_stopped']} Stopped Trails") + print(f" S3 Storage Total: {s3_gb:.2f} GB ({stats['s3_objects']:,} objects across {len(stats['s3_buckets'])} buckets)") + print(f" CloudWatch Storage Total: {cw_gb:.2f} GB (across {len(stats['cw_log_groups'])} log groups)") + + print(f"\n CloudWatch Retention Distribution:") + # Sort: Integers first, then strings like "Never Expire" + 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]} trail(s)") + print("-" * report_width) if __name__ == "__main__":