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 4da4cc49..3fdeef4d 100644 --- a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py @@ -1,65 +1,99 @@ -#!/usr/bin/env python - #!/usr/bin/env python3 import json import argparse -import re import sys -# --- VERSIONING --- -__version__ = "1.0.3" +__version__ = "1.0.5" def main(): - parser = argparse.ArgumentParser(description="AWS Config Audit Assessor") - parser.add_argument("--input", required=True, help="JSON audit file from check_config") - parser.add_argument("--central-bucket-regex", required=True, help="Regex for corporate S3 standards") + parser = argparse.ArgumentParser(description="AWS CloudTrail Audit Assessor") + parser.add_argument("--input", required=True, help="JSON file from check_cloudtrail") args = parser.parse_args() try: with open(args.input, 'r') as f: data = json.load(f) - except Exception as e: - print(f"Error: {e}"); sys.exit(1) + except: + print("Error reading input file"); sys.exit(1) - print("-" * 125) - print(f"{'Account ID':<15} | {'Global Status':<12} | {'S3 Compliance'}") - print("-" * 125) + print("-" * 160) + print(f"{'Account ID':<15} | {'Global Summary':<25} | {'Active/Stopped':<15} | {'Security/Cost Issues'}") + print("-" * 160) - stats = {"objects": 0, "bytes": 0, "total_accounts": len(data)} - unique_buckets = set() - non_central_buckets = set() + stats = { + "s3_bytes": 0, "s3_objects": 0, "s3_buckets": set(), + "cw_bytes": 0, "cw_groups": set(), + "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 + } for account in data: acc_id = account.get("account_id") checks = account.get("data", {}) summary = checks.get("account_summary", {}).get("_summary", "UNKNOWN") - s3_issues = [] - for reg, reg_data in checks.items(): - if reg == "account_summary": continue + acc_active = 0 + 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 - bucket = reg_data.get("s3_bucket", "N/A") - if bucket != "N/A": - unique_buckets.add(bucket) - stats["objects"] += reg_data.get("object_count", 0) - stats["bytes"] += reg_data.get("bucket_size_bytes", 0) - - if not re.match(args.central_bucket_regex, bucket): - s3_issues.append(bucket) - non_central_buckets.add(bucket) - - s3_status = f"NON_COMPLIANT ({len(s3_issues)} regions)" if s3_issues else "COMPLIANT" - print(f"{acc_id:<15} | {summary:<12} | {s3_status}") - - # Summary Section - size_gb = stats["bytes"] / (1024**3) - print("-" * 125) - print(f"ORGANIZATION STORAGE SUMMARY (CONFIG):") - print(f" Total S3 Storage: {size_gb:.2f} GB") - print(f" Total S3 Objects: {stats['objects']:,}") - print(f" Total S3 Buckets: {len(unique_buckets)}") - print(f" Non-Central Buckets: {len(non_central_buckets)}") - print("-" * 125) + # Status tracking + if val.get("is_logging") == "True": + acc_active += 1; stats["logging_active"] += 1 + else: + acc_stopped += 1; stats["logging_stopped"] += 1 + + # Type tracking + 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 + 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_groups"].add(val.get("trail_name")) + stats["cw_bytes"] += val["cw_logs_size_bytes"] + + # Safe SNS topic access + 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 + + print(f"{acc_id:<15} | {summary:<25} | {f'{acc_active} ON / {acc_stopped} OFF':<15} | {', '.join(issues) if issues else 'COMPLIANT'}") + + # Summary Footer + s3_gb = stats["s3_bytes"] / (1024**3) + cw_gb = stats["cw_bytes"] / (1024**3) + + print("-" * 160) + print(f"ORGANIZATION CLOUDTRAIL FOOTPRINT:") + 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" 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()): + print(f" - {r:<15}: {c}") + print("-" * 160) 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 4e47fed1..95626159 100644 --- a/local-app/python-tools/cross-organization/check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/check_cloudtrail.py @@ -2,7 +2,7 @@ import time from datetime import datetime, timedelta -__version__ = "1.1.0" +__version__ = "1.1.1" def get_s3_metrics(session, bucket_name, region): cw = session.client('cloudwatch', region_name=region) @@ -10,11 +10,11 @@ def get_s3_metrics(session, bucket_name, region): end = datetime.utcnow() start = end - timedelta(days=2) try: - # Check both Standard and All Storage types for full visibility 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'}], + 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']) @@ -50,8 +50,7 @@ def account_task(account_session, account_id, account_name, region): for reg in enabled_regions: ct = account_session.client('cloudtrail', region_name=reg) - # describe_trails with IncludeShadowTrails=True is powerful but we loop to catch - # trails that might be single-region in non-standard regions. + # IncludeShadowTrails catches regional replications of Org/Multi-region trails trails = ct.describe_trails(includeShadowTrails=True).get('trailList', []) for trail in trails: @@ -65,6 +64,7 @@ def account_task(account_session, account_id, account_name, region): else: local_trail_count += 1 t_name = trail['Name'] + # Use .get() for optional fields like SnsTopicARN and KmsKeyId t_data = { "trail_name": t_name, "home_region": trail.get('HomeRegion', reg), @@ -73,8 +73,9 @@ def account_task(account_session, account_id, account_name, region): "is_multi_region": str(trail.get('IsMultiRegionTrail', False)), "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'), - "check_elapsed_sec": 0 # Placeholder for loop timing if needed + "check_elapsed_sec": 0 } if 'CloudWatchLogsLogGroupArn' in trail: @@ -85,7 +86,7 @@ def account_task(account_session, account_id, account_name, region): results["data"][f"trail:{t_name}:{t_data['home_region']}"] = t_data - # Summary Logic + # Logic summary summary = "OK" if org_trail_count > 0 and local_trail_count > 0: summary = f"DUPLICATIVE/{local_trail_count}_LOCAL_WITH_ORG"