Skip to content

Commit

Permalink
update to include more config things
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Dec 31, 2025
1 parent 0c6919f commit b28839d
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 65 deletions.
62 changes: 54 additions & 8 deletions local-app/python-tools/test-cross-organization/check_config.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,66 @@
import boto3
from botocore.exceptions import ClientError

def account_task(account_session, account_id, account_name, region):
"""
Checks for AWS Config Global Resource Recording.
Task: Audits AWS Config across ALL enabled regions in the account.
Captures: Status, Retention, S3 Bucket, SNS Topic, and Delivery Frequency.
"""
res = {"alias": "None", "status": "SUCCESS"}
res = {"alias": "None", "status": "PENDING", "details": ""}

try:
# 1. Get IAM Alias
iam = account_session.client('iam')
aliases = iam.list_account_aliases().get('AccountAliases', [])
res["alias"] = aliases[0] if aliases else "None"

config = account_session.client('config', region_name=region)
recs = config.describe_configuration_recorders().get('ConfigurationRecorders', [])
# 2. Determine all active regions for this account
# We use EC2 to describe regions as it's the most reliable way to find 'opt-in' status
ec2 = account_session.client('ec2', region_name=region)
regions = [r['RegionName'] for r in ec2.describe_regions()['Regions']]

if any(r.get('recordingGroup', {}).get('includeGlobalResourceTypes') for r in recs):
res["status"] = "GLOBAL_ON"
else:
res["status"] = "GLOBAL_OFF"
report_lines = []
global_recorder_count = 0

for reg in regions:
reg_status = "OFF"
config = account_session.client('config', region_name=reg)

# Get Recorder Status
recorders = config.describe_configuration_recorders().get('ConfigurationRecorders', [])
delivery = config.describe_delivery_channels().get('DeliveryChannels', [])
retention = config.describe_retention_configurations().get('RetentionConfigurations', [])

# Default values
s3 = "N/A"
sns = "N/A"
freq = "N/A"
ret_days = "Default(7yrs)"

if recorders:
reg_status = "ON"
if recorders[0].get('recordingGroup', {}).get('includeGlobalResourceTypes'):
global_recorder_count += 1
reg_status = "ON+GLOBAL"

if delivery:
s3 = delivery[0].get('s3BucketName', 'N/A')
sns = delivery[0].get('snsTopicARN', 'N/A').split(':')[-1] # Shorten ARN
freq = delivery[0].get('configSnapshotDeliveryProperties', {}).get('deliveryFrequency', '24h')

if retention:
ret_days = f"{retention[0].get('RetentionPeriodInDays')} days"

report_lines.append(f"[{reg}: {reg_status} | S3:{s3} | Ret:{ret_days} | Freq:{freq}]")

# Summarize for the main table
res["status"] = f"OK({len(regions)}reg)" if global_recorder_count == 1 else f"WARN({global_recorder_count} Globals)"
res["details"] = " | ".join(report_lines)

except ClientError as e:
res["status"] = f"FAIL({e.response['Error']['Code']})"
except Exception as e:
res["status"] = "ERROR"

return res

110 changes: 53 additions & 57 deletions local-app/python-tools/test-cross-organization/org_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@
import sys
import logging
import time
import csv
import importlib
from datetime import datetime
from pathlib import Path
from botocore.exceptions import ClientError, ProfileNotFound

# --- VERSIONING ---
__version__ = "1.2.0"
__version__ = "1.2.2"

class OrgTaskRunner:
def __init__(self, args):
self.args = args
self.output_log = []
self.csv_data = [] # Stores structured data for CSV export
self.hierarchy_cache = {}
self.ou_summary = {}
self.start_time = 0
Expand All @@ -26,12 +28,10 @@ def __init__(self, args):
self.org_id = "unknown"

def log_print(self, message=""):
"""Prints to console and buffers for file output."""
print(message)
self.output_log.append(message)

def get_full_path(self, org_client, entity_id):
"""Resolves hierarchy path and caches results. Strips 'Root'."""
if entity_id in self.hierarchy_cache:
return self.hierarchy_cache[entity_id]
if entity_id.startswith('r-'):
Expand All @@ -50,16 +50,15 @@ def get_full_path(self, org_client, entity_id):
return f"Error({entity_id})", entity_id

def load_tasks(self):
"""Dynamically imports account_task from provided module names."""
tasks = []
if self.args.enable_checks:
sys.path.append(os.getcwd())
for module_name in self.args.enable_checks:
try:
module = importlib.import_module(module_name)
m_name = module_name.replace('.py', '')
module = importlib.import_module(m_name)
if hasattr(module, 'account_task'):
tasks.append((module_name, getattr(module, 'account_task')))
else:
self.log_print(f"Warning: {module_name} missing 'account_task' function.")
tasks.append((m_name, getattr(module, 'account_task')))
except ImportError as e:
self.log_print(f"Error importing {module_name}: {e}")
return tasks
Expand All @@ -73,7 +72,6 @@ def run(self):
self.session = boto3.Session(profile_name=self.args.profile, region_name=self.args.region)
sts = self.session.client('sts')
org = self.session.client('organizations')

caller = sts.get_caller_identity()
self.partition = caller['Arn'].split(':')[1]

Expand All @@ -84,47 +82,39 @@ def run(self):
tasks = self.load_tasks()

# Filename Logic
final_filename = None
if self.args.output:
ds = datetime.now().strftime("%Y-%m-%dT%H%M%S")
if self.args.output == 'DEFAULT':
final_filename = f"results_{self.org_id}.{ds}.txt"
else:
p = Path(self.args.output)
final_filename = f"{p.stem}_{ds}{p.suffix}"
ds = datetime.now().strftime("%Y-%m-%dT%H%M%S")
base_name = self.args.output if self.args.output and self.args.output != 'DEFAULT' else f"results_{self.org_id}"
txt_file = f"{base_name}.{ds}.txt"
csv_file = f"{base_name}.{ds}.csv"

self.log_print("-" * 100)
self.log_print(f"AWS ORG TASK RUNNER - Version {__version__}")
self.log_print("-" * 100)
self.log_print(f" Profile: {self.args.profile or 'Default'}")
self.log_print(f" Identity: {caller['Arn']}")
self.log_print(f" Checks: {[t[0] for t in tasks] if tasks else 'Connectivity Only'}")
if final_filename: self.log_print(f" Output File: {final_filename}")
self.log_print(f"AWS ORG TASK RUNNER - v{__version__} | Partition: {self.partition}")
self.log_print(f"Source Identity: {caller['Arn']}")
self.log_print("-" * 100)

all_accounts = []
paginator = org.get_paginator('list_accounts')
for page in paginator.paginate():
all_accounts.extend([acc for acc in page['Accounts'] if acc['Status'] == 'ACTIVE'])

sort_key = 'Name' if self.args.sort == 'name' else 'Id'
all_accounts.sort(key=lambda x: x[sort_key].lower())
all_accounts.sort(key=lambda x: x['Name' if self.args.sort == 'name' else 'Id'].lower())

self.log_print(f"{'#':<4} | {'Account Name':<25} | {'Alias':<40} | {'Account ID':<15} | {'Status':<12} | {'Full OU Path'}")
self.log_print("-" * 180)
# Header for Terminal (Truncated for readability)
self.log_print(f"{'#':<4} | {'Account Name':<25} | {'Account ID':<15} | {'Status':<12} | {'OU Path'}")
self.log_print("-" * 120)

for i, acc in enumerate(all_accounts, 1):
acc_id, acc_name = acc['Id'], acc['Name']
role_arn = f"arn:{self.partition}:iam::{acc_id}:role/{self.args.role_name}"

parents = org.list_parents(ChildId=acc_id).get('Parents', [])
raw_path, ou_id = self.get_full_path(org, parents[0]['Id']) if parents else ("Orphaned", "N/A")
ou_path = raw_path if raw_path else "N/A (Root)"
ou_path = raw_path if raw_path else "Root"

sum_key = (ou_path, ou_id)
if sum_key not in self.ou_summary: self.ou_summary[sum_key] = {"success": 0, "fail": 0}

alias, status_text = "None", "PENDING"
alias, status_text, details = "N/A", "PENDING", ""
try:
assumed = sts.assume_role(RoleArn=role_arn, RoleSessionName="OrgRunnerTask")
m_sess = boto3.Session(
Expand All @@ -134,40 +124,45 @@ def run(self):
region_name=self.session.region_name
)

# Default connectivity check + dynamic tasks
if not tasks:
aliases = m_sess.client('iam').list_account_aliases().get('AccountAliases', [])
alias = aliases[0] if aliases else "None"
status_text = "SUCCESS"
else:
for t_name, t_func in tasks:
res = t_func(m_sess, acc_id, acc_name, self.session.region_name)
status_text = res.get('status', 'SUCCESS')
alias = res.get('alias', 'N/A')
for t_name, t_func in tasks:
res = t_func(m_sess, acc_id, acc_name, self.session.region_name)
status_text = res.get('status', 'SUCCESS')
alias = res.get('alias', 'N/A')
details = res.get('details', '')

self.ou_summary[sum_key]["success"] += 1
except ClientError as e:
status_text = f"FAIL({e.response['Error']['Code']})"
except Exception as e:
status_text = "ASSUME_FAIL"
details = str(e)
self.ou_summary[sum_key]["fail"] += 1

self.log_print(f"{i:<4} | {acc_name[:25]:<25} | {alias[:40]:<40} | {acc_id:<15} | {status_text:<12} | {ou_path}")
# Log to terminal (keeping it clean)
self.log_print(f"{i:<4} | {acc_name[:25]:<25} | {acc_id:<15} | {status_text:<12} | {ou_path}")

# Capture full data for CSV (untruncated)
self.csv_data.append({
"Item": i, "AccountName": acc_name, "AccountID": acc_id,
"Alias": alias, "Status": status_text, "OU_Path": ou_path, "OU_ID": ou_id,
"Details": details
})

# End Summary
self.log_print("\n" + "=" * 80)
self.log_print(f"SCAN COMPLETE | Time: {int(time.perf_counter() - self.start_time)}s")
self.log_print("=" * 80)

# Summary Table
end_time = time.perf_counter()
el = divmod(int(end_time - self.start_time), 60)

self.log_print("\n" + "=" * 85)
self.log_print(f"{'OU PATH SUMMARY':<45} | {'OU ID':<18} | {'SUCC':<6} | {'FAIL':<6}")
self.log_print("=" * 85)
for (path, o_id), counts in sorted(self.ou_summary.items()):
self.log_print(f"{path[:45]:<45} | {o_id:<18} | {counts['success']:<6} | {counts['fail']:<6}")
self.log_print("=" * 85)
self.log_print(f"Time: {el[0]}m {el[1]}s | Accounts: {len(all_accounts)}")
self.log_print("=" * 85)

if final_filename:
with open(final_filename, 'w') as f:
if self.args.output:
# Save TXT (Log)
with open(txt_file, 'w') as f:
f.write("\n".join(self.output_log))

# Save CSV (Data)
with open(csv_file, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=self.csv_data[0].keys())
writer.writeheader()
writer.writerows(self.csv_data)

print(f"Reports saved:\n - {txt_file}\n - {csv_file}")

except Exception as e:
print(f"CRITICAL ERROR: {e}")
Expand All @@ -182,3 +177,4 @@ def run(self):
p.add_argument("--enable-checks", nargs='+')
p.add_argument("--debug", action="store_true")
OrgTaskRunner(p.parse_args()).run()

0 comments on commit b28839d

Please sign in to comment.