Skip to content

Commit

Permalink
update 1.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Jan 2, 2026
1 parent f4e3bf9 commit 3ad15a0
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 174 deletions.
65 changes: 25 additions & 40 deletions local-app/python-tools/cross-organization/check_config.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,51 @@
import boto3
from botocore.exceptions import ClientError

def account_task(account_session, account_id, account_name, region):
"""
Task: Audits AWS Config across ALL enabled regions in the account.
Captures: Status, Retention, S3 Bucket, SNS Topic, and Delivery Frequency.
Task: Deep Audit of AWS Config across all regions.
Returns: Data structured by region for JSON/CSV processing.
"""
res = {"alias": "None", "status": "PENDING", "details": ""}
results = {"alias": "None", "status": "SUCCESS", "data": {}}

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"
results["alias"] = aliases[0] if aliases else "None"

# 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']]

report_lines = []
global_recorder_count = 0

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

# Get Recorder Status
# Fetch Config state
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)"

# Store regional metrics
reg_data = {
"recorder_enabled": "False",
"global_resources": "False",
"s3_bucket": "N/A",
"sns_topic": "N/A",
"delivery_freq": "N/A",
"retention_days": "2555 (7yr)"
}

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

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')
reg_data["s3_bucket"] = delivery[0].get('s3BucketName', 'N/A')
reg_data["sns_topic"] = delivery[0].get('snsTopicARN', 'N/A').split(':')[-1]
reg_data["delivery_freq"] = delivery[0].get('configSnapshotDeliveryProperties', {}).get('deliveryFrequency', '24h')

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

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']})"
results["data"][reg] = reg_data

except Exception as e:
res["status"] = "ERROR"
results["status"] = f"ERROR: {str(e)}"

return res

return results
200 changes: 66 additions & 134 deletions local-app/python-tools/cross-organization/org_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,168 +4,102 @@
import argparse
import os
import sys
import logging
import time
import json
import csv
import time
import importlib
from datetime import datetime
from pathlib import Path
from botocore.exceptions import ClientError, ProfileNotFound

# --- VERSIONING ---
__version__ = "1.2.2"
__version__ = "1.3.0"

class OrgTaskRunner:
def __init__(self, args):
self.args = args
self.output_log = []
self.csv_data = [] # Stores structured data for CSV export
self.full_results = [] # Internal list of dicts for JSON/CSV
self.hierarchy_cache = {}
self.ou_summary = {}
self.start_time = 0
self.partition = "aws"
self.session = None
self.org_id = "unknown"

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

def get_full_path(self, org_client, entity_id):
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)
return None, entity_id
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')
org = session.client('organizations')

try:
parents = org_client.list_parents(ChildId=entity_id)['Parents']
p_id = parents[0]['Id'] if parents else None
ou_desc = org_client.describe_organizational_unit(OrganizationalUnitId=entity_id)
ou_name = ou_desc['OrganizationalUnit']['Name']
p_path, _ = self.get_full_path(org_client, p_id) if p_id else (None, None)
full_path = f"{p_path}:{ou_name}" if p_path else ou_name
self.hierarchy_cache[entity_id] = (full_path, entity_id)
return full_path, entity_id
except:
return f"Error({entity_id})", entity_id

def load_tasks(self):
self.org_id = org.describe_organization()['Organization']['Id']
except: pass

# Load dynamic tasks
tasks = []
if self.args.enable_checks:
sys.path.append(os.getcwd())
for module_name in self.args.enable_checks:
try:
m_name = module_name.replace('.py', '')
module = importlib.import_module(m_name)
if hasattr(module, 'account_task'):
tasks.append((m_name, getattr(module, 'account_task')))
except ImportError as e:
self.log_print(f"Error importing {module_name}: {e}")
return tasks

def run(self):
self.start_time = time.perf_counter()
if self.args.debug:
boto3.set_stream_logger(name='botocore', level=logging.DEBUG)

try:
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]
for m in self.args.enable_checks:
module = importlib.import_module(m.replace('.py', ''))
tasks.append(getattr(module, 'account_task'))

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'])

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

self.log_print(f"--- Starting Org Run (v{__version__}) | Accounts: {len(all_accounts)} ---")

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

try:
self.org_id = org.describe_organization()['Organization']['Id']
except: pass
assumed = sts.assume_role(RoleArn=role_arn, RoleSessionName="OrgRunnerTask")
m_sess = boto3.Session(
aws_access_key_id=assumed['Credentials']['AccessKeyId'],
aws_secret_access_key=assumed['Credentials']['SecretAccessKey'],
aws_session_token=assumed['Credentials']['SessionToken'],
region_name=self.args.region
)

account_data = {"account_id": acc_id, "account_name": acc_name, "checks": {}}

for t_func in tasks:
# Capture raw data from task
res = t_func(m_sess, acc_id, acc_name, self.args.region)
account_data["alias"] = res.get("alias")
account_data["checks"] = res.get("data", {}) # Expecting a dict of regions

tasks = self.load_tasks()

# Filename Logic
self.full_results.append(account_data)
self.log_print(f"[{i}/{len(all_accounts)}] Processed {acc_name}")

except Exception as e:
self.log_print(f"[{i}/{len(all_accounts)}] FAILED {acc_name}: {e}")

# EXPORTS
if self.args.output:
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 - 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'])
base = self.args.output if self.args.output != 'DEFAULT' else f"audit_{self.org_id}"

all_accounts.sort(key=lambda x: x['Name' if self.args.sort == 'name' else 'Id'].lower())
# 1. Save JSON
with open(f"{base}.{ds}.json", 'w') as f:
json.dump(self.full_results, f, indent=2)

# 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 "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, details = "N/A", "PENDING", ""
try:
assumed = sts.assume_role(RoleArn=role_arn, RoleSessionName="OrgRunnerTask")
m_sess = boto3.Session(
aws_access_key_id=assumed['Credentials']['AccessKeyId'],
aws_secret_access_key=assumed['Credentials']['SecretAccessKey'],
aws_session_token=assumed['Credentials']['SessionToken'],
region_name=self.session.region_name
)

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 Exception as e:
status_text = "ASSUME_FAIL"
details = str(e)
self.ou_summary[sum_key]["fail"] += 1

# 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)

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}")
# 2. Save CSV (Long Format)
with open(f"{base}.{ds}.csv", 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(["account_id", "account_alias", "region", "field_name", "field_value"])
for acc in self.full_results:
for reg, fields in acc["checks"].items():
for key, val in fields.items():
writer.writerow([acc["account_id"], acc.get("alias"), reg, key, val])

except Exception as e:
print(f"CRITICAL ERROR: {e}")
self.log_print(f"\nExports complete: {base}.{ds}.json and .csv")

if __name__ == "__main__":
p = argparse.ArgumentParser()
Expand All @@ -175,6 +109,4 @@ def run(self):
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("--debug", action="store_true")
OrgTaskRunner(p.parse_args()).run()

0 comments on commit 3ad15a0

Please sign in to comment.