Skip to content

Commit

Permalink
add summary
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Jan 2, 2026
1 parent 34a3f17 commit bbc5eca
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 90 deletions.
Empty file.
35 changes: 16 additions & 19 deletions local-app/python-tools/cross-organization/check_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,41 @@ def get_s3_metrics(session, bucket_name, region):
end = datetime.utcnow()
start = end - timedelta(days=2)
try:
response = cw.get_metric_statistics(
Namespace='AWS/S3', MetricName='BucketSizeBytes',
Dimensions=[{'Name': 'BucketName', 'Value': bucket_name}, {'Name': 'StorageType', 'Value': 'StandardStorage'}],
StartTime=start, EndTime=end, Period=86400, Statistics=['Average']
)
if response['Datapoints']: metrics["bucket_size_bytes"] = int(response['Datapoints'][-1]['Average'])

response = cw.get_metric_statistics(
Namespace='AWS/S3', MetricName='NumberOfObjects',
Dimensions=[{'Name': 'BucketName', 'Value': bucket_name}, {'Name': 'StorageType', 'Value': 'AllStorageTypes'}],
StartTime=start, EndTime=end, Period=86400, Statistics=['Average']
)
if response['Datapoints']: metrics["object_count"] = int(response['Datapoints'][-1]['Average'])
r1 = cw.get_metric_statistics(Namespace='AWS/S3', MetricName='BucketSizeBytes', Dimensions=[{'Name': 'BucketName', 'Value': bucket_name}, {'Name': 'StorageType', 'Value': 'StandardStorage'}], StartTime=start, EndTime=end, Period=86400, Statistics=['Average'])
if r1['Datapoints']: metrics["bucket_size_bytes"] = int(r1['Datapoints'][-1]['Average'])
r2 = cw.get_metric_statistics(Namespace='AWS/S3', MetricName='NumberOfObjects', Dimensions=[{'Name': 'BucketName', 'Value': bucket_name}, {'Name': 'StorageType', 'Value': 'AllStorageTypes'}], StartTime=start, EndTime=end, Period=86400, Statistics=['Average'])
if r2['Datapoints']: metrics["object_count"] = int(r2['Datapoints'][-1]['Average'])
except: pass
return metrics

def account_task(account_session, account_id, account_name, region):
results = {"alias": "N/A", "data": {}}
global_count = 0
try:
results["alias"] = account_session.client('iam').list_account_aliases().get('AccountAliases', ["N/A"])[0]
regions = [r['RegionName'] for r in account_session.client('ec2', region_name=region).describe_regions()['Regions']]

for reg in regions:
reg_start = time.perf_counter()
config = account_session.client('config', region_name=reg)
channels = config.describe_delivery_channels().get('DeliveryChannels', [])
recorders = config.describe_configuration_recorders().get('ConfigurationRecorders', [])
channels = config.describe_delivery_channels().get('DeliveryChannels', [])

reg_data = {"recorder_status": "ON" if recorders else "OFF", "s3_bucket": "N/A"}
is_global = any(r.get('recordingGroup', {}).get('includeGlobalResourceTypes') for r in recorders)
if is_global: global_count += 1

reg_data = {"recorder_status": "ON" if recorders else "OFF", "global_recording": str(is_global)}
if channels:
bucket = channels[0].get('s3BucketName', 'N/A')
reg_data.update({
"s3_bucket": bucket,
"delivery_freq": channels[0].get('configSnapshotDeliveryProperties', {}).get('deliveryFrequency', 'N/A')
})
reg_data.update({"s3_bucket": bucket, "delivery_freq": channels[0].get('configSnapshotDeliveryProperties', {}).get('deliveryFrequency', 'N/A')})
if bucket != "N/A": reg_data.update(get_s3_metrics(account_session, bucket, reg))

reg_data["check_elapsed_sec"] = round(time.perf_counter() - reg_start, 3)
results["data"][reg] = reg_data

# Add Per-Account Summary record (using 'global' as the region key for the CSV row)
summary_val = f"OK/1" if global_count == 1 else f"MULTIPLE/{global_count}" if global_count > 1 else "NONE/0"
results["data"]["account_summary"] = {"_summary": summary_val}

except Exception as e: results["error"] = str(e)
return results
111 changes: 40 additions & 71 deletions local-app/python-tools/cross-organization/org_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,23 @@
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed

# tqdm integration
try:
from tqdm import tqdm
except ImportError:
print("Warning: 'tqdm' not found. Install with 'pip install tqdm'.")
def tqdm(iterable, **kwargs): return iterable

# --- VERSIONING ---
__version__ = "1.5.4"
__version__ = "1.6.0"

class OrgTaskRunner:
def __init__(self, args):
self.args = args
self.output_log = []
self.full_results = []
self.hierarchy_cache = {}
self.created_files = []
self.start_time = 0

def log_print(self, message=""):
self.output_log.append(message)

def get_ou_path(self, org_client, entity_id):
"""Recursive OU path resolver with caching."""
if entity_id in self.hierarchy_cache: return self.hierarchy_cache[entity_id]
if entity_id.startswith('r-'):
self.hierarchy_cache[entity_id] = (None, entity_id)
Expand All @@ -51,24 +44,18 @@ def get_ou_path(self, org_client, entity_id):
except: return "Unknown", entity_id

def process_account(self, acc, partition, tasks):
"""Worker thread for each account."""
thread_session = boto3.Session(profile_name=self.args.profile, region_name=self.args.region)
sts, org = thread_session.client('sts'), thread_session.client('organizations')

acc_id, acc_name = acc['Id'], acc['Name']
role_arn = f"arn:{partition}:iam::{acc_id}:role/{self.args.role_name}"

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

account_data = {
"account_id": acc_id,
"account_name": acc_name,
"alias": "N/A",
"ou_path": ou_path,
"ou_id": ou_id,
"checks": {}
"account_id": acc_id, "account_name": acc_name, "alias": "N/A",
"ou_path": ou_path, "ou_id": ou_id, "checks": {}
}

try:
Expand All @@ -79,91 +66,73 @@ def process_account(self, acc, partition, tasks):
aws_session_token=assumed['Credentials']['SessionToken'],
region_name=self.args.region
)

for t_func in tasks:
res = t_func(m_sess, acc_id, acc_name, self.args.region)
account_data["alias"] = res.get("alias", "N/A")
account_data["checks"].update(res.get("data", {}))

return account_data, None
except Exception as e:
return None, f"FAILED {acc_name} ({acc_id}): {str(e)}"
return None, f"FAILED {acc_name}: {str(e)}"

def run(self):
self.start_time = time.perf_counter()
session = boto3.Session(profile_name=self.args.profile, region_name=self.args.region)
sts = session.client('sts')
partition = sts.get_caller_identity()['Arn'].split(':')[1]
partition = session.client('sts').get_caller_identity()['Arn'].split(':')[1]

# Load tasks & build dynamic filename suffix
tasks, check_names = [], []
if self.args.enable_checks:
sys.path.append(os.getcwd())
for m in self.args.enable_checks:
module_name = m.replace('.py', '')
module = importlib.import_module(module_name)
module = importlib.import_module(m.replace('.py', ''))
tasks.append(getattr(module, 'account_task'))
check_names.append(module_name)
check_names.append(m.replace('.py', ''))

check_suffix = ".".join(check_names) if check_names else "connectivity"

all_accounts = [acc for page in session.client('organizations').get_paginator('list_accounts').paginate()
for acc in page['Accounts'] if acc['Status'] == 'ACTIVE']
all_accounts.sort(key=lambda x: x['Name' if self.args.sort == 'name' else 'Id'].lower())

# UPDATED HEADER
print("-" * 100)
print(f"AWS ORG TASK RUNNER - v{__version__}")
print(f"Runners (Max Workers): {self.args.max_workers}")
print(f"Active Check: {check_suffix}")
print("-" * 100)

print("-" * 60)
print(f"AWS ORG TASK RUNNER - v{__version__} | Runners: {self.args.max_workers}")
print("-" * 60)

with ThreadPoolExecutor(max_workers=self.args.max_workers) as executor:
futures = {executor.submit(self.process_account, acc, partition, tasks): acc for acc in all_accounts}
with tqdm(total=len(all_accounts), desc="Processing", unit="acc", colour="green") as pbar:
for future in as_completed(futures):
data, err = future.result()
for f in as_completed(futures):
data, _ = f.result()
if data: self.full_results.append(data)
pbar.update(1)

# FINAL EXPORTS
if self.args.output:
ds = datetime.now().strftime("%Y-%m-%dT%H%M%S")
base = self.args.output if self.args.output != 'DEFAULT' else f"audit_results.{check_suffix}"

# Save CSV
csv_file = f"{base}.{ds}.csv"
with open(csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(["account_id", "account_name", "account_alias", "ou_path", "ou_id", "region", "field_name", "field_value"])
for acc in self.full_results:
for reg, fields in acc["checks"].items():
for k, v in fields.items():
writer.writerow([acc["account_id"], acc["account_name"], acc["alias"], acc["ou_path"], acc["ou_id"], reg, k, v])
self.created_files.append(csv_file)

# Save JSON
json_file = f"{base}.{ds}.json"
with open(json_file, 'w') as f:
json.dump(self.full_results, f, indent=2)
self.created_files.append(json_file)

# UPDATED TRAILER
print("\n" + "=" * 100)
print(f"Run Complete | Time: {round(time.perf_counter() - self.start_time, 2)}s")
if self.created_files:
print("Files Created:")
for f in self.created_files:
print(f" - {f}")
print("=" * 100)
ds = datetime.now().strftime("%Y%m%dT%H%M%S")
# 1. ACCOUNT BASELINE FILES
acc_base = f"audit_results.account.{ds}"
with open(f"{acc_base}.json", 'w') as f: json.dump(self.full_results, f, indent=2)
with open(f"{acc_base}.csv", 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=["account_id", "account_name", "alias", "ou_path", "ou_id"])
w.writeheader()
w.writerows([{k: v for k, v in acc.items() if k != 'checks'} for acc in self.full_results])
self.created_files.extend([f"{acc_base}.json", f"{acc_base}.csv"])

# 2. CHECK SPECIFIC FILES
for cn in check_names:
chk_base = f"audit_results.{cn}.{ds}"
with open(f"{chk_base}.csv", 'w', newline='') as f:
w = csv.writer(f)
w.writerow(["account_id", "region", "field_name", "field_value"])
for acc in self.full_results:
for reg, fields in acc["checks"].items():
for k, v in fields.items():
w.writerow([acc["account_id"], reg, k, v])
self.created_files.append(f"{chk_base}.csv")

print(f"\nTime: {round(time.perf_counter() - self.start_time, 2)}s\nFiles Created:")
for f in self.created_files: print(f" - {f}")

if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("--profile", default=os.environ.get("AWS_PROFILE"))
p.add_argument("--region", default="us-east-1")
p.add_argument("--role-name", required=True)
p.add_argument("--sort", choices=['id', 'name'], default='name')
p.add_argument("--output", nargs='?', const='DEFAULT')
p.add_argument("--enable-checks", nargs='+')
p.add_argument("--max-workers", type=int, default=8)
p.add_argument("--profile"); p.add_argument("--region", default="us-east-1"); p.add_argument("--sort", default="name")
OrgTaskRunner(p.parse_args()).run()

0 comments on commit bbc5eca

Please sign in to comment.