Skip to content

Commit

Permalink
home region check
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Jan 2, 2026
1 parent cd10d19 commit ecd8750
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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

Expand Down Expand Up @@ -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__":
Expand Down
75 changes: 27 additions & 48 deletions local-app/python-tools/cross-organization/check_cloudtrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}}
Expand All @@ -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'),
Expand All @@ -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)
Expand Down

0 comments on commit ecd8750

Please sign in to comment.