From bfb2e3652db15f845af257c3418f918f9cc7917b Mon Sep 17 00:00:00 2001 From: badra001 Date: Thu, 15 Jan 2026 10:09:06 -0500 Subject: [PATCH] add --verbose --- .../cross-organization/tag-checker.py | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/local-app/python-tools/cross-organization/tag-checker.py b/local-app/python-tools/cross-organization/tag-checker.py index 3f25ce72..d55993fb 100755 --- a/local-app/python-tools/cross-organization/tag-checker.py +++ b/local-app/python-tools/cross-organization/tag-checker.py @@ -7,13 +7,12 @@ import sys import time import re -import os from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed from botocore.exceptions import ClientError from tqdm import tqdm -__version__ = "1.1.1" +__version__ = "1.1.2" def get_args(): parser = argparse.ArgumentParser(description=f"AWS Org Tag Scanner v{__version__}") @@ -22,13 +21,14 @@ def get_args(): parser.add_argument("--profile", required=True, help="AWS CLI profile for Management Account") parser.add_argument("--tags-file", required=True, help="CSV file with Tag Key in the first column") parser.add_argument("--max-workers", type=int, default=8, help="Max concurrent account scans (default: 8)") - parser.add_argument("--account-regex", help="Regex to filter accounts by alias") + parser.add_argument("--account-regex", help="Regex to filter accounts by alias (case-insensitive)") parser.add_argument("--accounts-from", help="File of Account IDs to process (one per line)") parser.add_argument("--output", default="tag_checker_findings", help="Prefix for output files") parser.add_argument("--limit", type=int, default=0, help="Limit total accounts processed") + parser.add_argument("--verbose", action="store_true", help="Enable detailed logging and error reporting") return parser.parse_args() -def get_session(management_session, account_id, role_name, partition): +def get_session(management_session, account_id, role_name, partition, verbose): sts = management_session.client('sts') role_arn = f"arn:{partition}:iam::{account_id}:role/{role_name}" try: @@ -37,35 +37,49 @@ def get_session(management_session, account_id, role_name, partition): return boto3.Session(aws_access_key_id=c['AccessKeyId'], aws_secret_access_key=c['SecretAccessKey'], aws_session_token=c['SessionToken']) - except: return None + except Exception as e: + if verbose: + print(f"\n[!] Auth Error for {account_id}: {str(e)}") + return None -def scan_account(account, management_session, role_name, partition, tag_keys, region_name, lane_id, account_regex): +def scan_account(account, management_session, role_name, partition, tag_keys, region_name, lane_id, account_regex, verbose): acc_id = account['Id'] - m_session = get_session(management_session, acc_id, role_name, partition) + m_session = get_session(management_session, acc_id, role_name, partition, verbose) if not m_session: - return [], acc_id, "N/A", f"Skipped: Cannot assume role" + return [], acc_id, "N/A", "Skipped: Auth/Session Failure" - # CRITICAL: Retrieve the true alias from the target account + # Precise Alias Retrieval with Verbose Error Tracking + alias = "N/A" try: - alias_list = m_session.client('iam').list_account_aliases().get('AccountAliases', []) + iam_client = m_session.client('iam') + alias_resp = iam_client.list_account_aliases() + alias_list = alias_resp.get('AccountAliases', []) + + if verbose: + # Report the raw list of aliases returned + tqdm.write(f"[DEBUG] {acc_id} | Raw Aliases Found: {alias_list}") + alias = alias_list[0] if alias_list else "N/A" - except Exception: - alias = "N/A" + except Exception as e: + alias = f"ERROR: {type(e).__name__}" + if verbose: + tqdm.write(f"[ERROR] {acc_id} | Failed to pull alias: {str(e)}") - # Regex Filter check + # Check Regex on the actual alias if account_regex and not re.search(account_regex, alias, re.IGNORECASE): return [], acc_id, alias, f"Skipped: Regex mismatch ({alias})" findings = [] - # Progress bar with Account ID and Alias pbar = tqdm(total=len(tag_keys), desc=f"Lane {lane_id} | {acc_id} ({alias})", position=lane_id, leave=False, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}') try: ec2 = m_session.client('ec2', region_name=region_name) regions = [r['RegionName'] for r in ec2.describe_regions()['Regions']] - except: regions = [region_name] + except Exception as e: + if verbose: tqdm.write(f"[DEBUG] {acc_id} | Region fetch failed, using default: {str(e)}") + regions = [region_name] for key in tag_keys: for r in regions: @@ -81,6 +95,7 @@ def scan_account(account, management_session, role_name, partition, tag_keys, re }) except ClientError as e: if "Throttling" in str(e): time.sleep(1) + elif verbose: tqdm.write(f"[DEBUG] {acc_id} | TaggingAPI Error in {r}: {str(e)}") pbar.update(1) pbar.close() @@ -95,6 +110,7 @@ def main(): org = session.client('organizations') partition = session.client('sts').get_caller_identity()['Arn'].split(':')[1] + # Load Input Data with open(args.tags_file, mode='r', encoding='utf-8-sig') as f: tag_keys = [row[0].strip() for row in list(csv.reader(f))[1:] if row] @@ -103,10 +119,10 @@ def main(): with open(args.accounts_from, 'r') as f: target_ids = [l.strip() for l in f if l.strip()] - print(f"\n{'='*60}\nAWS TAG CHECKER v{__version__}\n{'='*60}") - print(f"Profile: {args.profile} | Region: {args.region} | Role: {args.role_name}") + print(f"\n{'='*70}\nAWS TAG CHECKER v{__version__}\n{'='*70}") + print(f"Profile: {args.profile} | Region: {args.region} | Workers: {args.max_workers}") + if args.verbose: print("[!] Verbose Logging Enabled") - # Get all accounts from Org all_accs = [] paginator = org.get_paginator('list_accounts') for page in paginator.paginate(): @@ -116,28 +132,27 @@ def main(): all_accs.append(a) if args.limit > 0: all_accs = all_accs[:args.limit] - print(f"Workers: {args.max_workers} | Tags: {len(tag_keys)} | Targeted Accounts: {len(all_accs)}") - print(f"{'='*60}\n") + print(f"Tags: {len(tag_keys)} | Targeted Accounts: {len(all_accs)}") + print(f"{'='*70}\n") all_findings = [] overall_pbar = tqdm(total=len(all_accs), desc="Total Org Progress", position=0) with ThreadPoolExecutor(max_workers=args.max_workers) as executor: - # Lanes 1 through max_workers futures = {executor.submit(scan_account, acc, session, args.role_name, partition, tag_keys, args.region, (i % args.max_workers) + 1, - args.account_regex): acc for i, acc in enumerate(all_accs)} + args.account_regex, args.verbose): acc for i, acc in enumerate(all_accs)} for future in as_completed(futures): res, acc_id, alias, status = future.result() - if "Skipped" in status or "Skipping" in status: - overall_pbar.write(f"[-] {acc_id}: {status}") + if "Skipped" in status: + overall_pbar.write(f"[-] {acc_id} ({alias}): {status}") else: all_findings.extend(res) overall_pbar.update(1) overall_pbar.close() - print("\n" * (args.max_workers + 1)) # Clear lanes + print("\n" * (args.max_workers + 1)) csv_out = f"{args.output}_{ts}.csv" if all_findings: