diff --git a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py old mode 100644 new mode 100755 diff --git a/local-app/python-tools/cross-organization/org_runner.py b/local-app/python-tools/cross-organization/org_runner.py index 0c227264..7f3c1877 100755 --- a/local-app/python-tools/cross-organization/org_runner.py +++ b/local-app/python-tools/cross-organization/org_runner.py @@ -18,7 +18,7 @@ def tqdm(iterable, **kwargs): return iterable # --- VERSIONING --- -__version__ = "1.6.4" +__version__ = "1.6.5" class OrgTaskRunner: def __init__(self, args): @@ -30,7 +30,6 @@ def __init__(self, args): self.org_id = "unknown" def get_ou_path(self, org_client, entity_id): - """Recursively resolves OU path and caches results.""" 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) @@ -47,7 +46,6 @@ def get_ou_path(self, org_client, entity_id): except: return "Unknown", entity_id def process_account(self, acc, partition, tasks): - """Worker thread logic with isolated task data storage.""" 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'] @@ -88,33 +86,51 @@ def run(self): self.start_time = time.perf_counter() session = boto3.Session(profile_name=self.args.profile, region_name=self.args.region) org_client = session.client('organizations') - partition = session.client('sts').get_caller_identity()['Arn'].split(':')[1] + sts_client = session.client('sts') + iam_client = session.client('iam') + + # Resolve Header Info + caller = sts_client.get_caller_identity() + partition = caller['Arn'].split(':')[1] + + org_info = org_client.describe_organization()['Organization'] + self.org_id = org_info['Id'] + master_id = org_info['MasterAccountId'] try: - self.org_id = org_client.describe_organization()['Organization']['Id'] - except: pass + master_aliases = iam_client.list_account_aliases()['AccountAliases'] + master_alias = master_aliases[0] if master_aliases else "None" + except: master_alias = "Unknown (Check Permissions)" + # Load tasks & build dynamic metadata tasks, check_info = [], [] if self.args.enable_checks: sys.path.append(os.getcwd()) for m in self.args.enable_checks: mod_name = m.replace('.py', '') module = importlib.import_module(mod_name) - v = getattr(module, '__version__', '?.?.?') tasks.append((mod_name, getattr(module, 'account_task'))) + v = getattr(module, '__version__', '?.?.?') check_info.append(f"{mod_name} (v{v})") all_accounts = [acc for page in org_client.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"Org ID: {self.org_id}") - print(f"Target Role: {self.args.role_name}") - print(f"Runners: {self.args.max_workers}") - print(f"Enabled Checks: {', '.join(check_info)}") - print(f"Accounts Found: {len(all_accounts)}") + print(f" Profile: {self.args.profile or 'default'}") + print(f" Region: {self.args.region}") + print(f" Caller Identity: {caller['Arn']}") + print(f" Organization ID: {self.org_id}") + print(f" Management ID: {master_id}") + print(f" Management Alias: {master_alias}") + print("-" * 100) + print(f" Target Role: {self.args.role_name}") + print(f" Max Workers: {self.args.max_workers}") + print(f" Enabled Checks: {', '.join(check_info) if check_info else 'None'}") + print(f" Accounts Found: {len(all_accounts)}") print("-" * 100) with ThreadPoolExecutor(max_workers=self.args.max_workers) as executor: @@ -128,7 +144,7 @@ def run(self): if self.args.output: ds = datetime.now().strftime("%Y%m%dT%H%M%S") - # --- RESTORED ACCOUNT BASELINE --- + # 1. ACCOUNT BASELINE (RESTORED) acc_base = f"audit_results.account.{ds}" with open(f"{acc_base}.json", 'w') as f: json.dump([r['metadata'] for r in self.full_results], f, indent=2) @@ -138,7 +154,7 @@ def run(self): w.writerows([r['metadata'] for r in self.full_results]) self.created_files.extend([f"{acc_base}.json", f"{acc_base}.csv"]) - # --- CHECK SPECIFIC FILES --- + # 2. CHECK SPECIFIC FILES for mod_name, _ in tasks: chk_base = f"audit_results.{mod_name}.{ds}" with open(f"{chk_base}.csv", 'w', newline='') as f: