Skip to content

Commit

Permalink
add --verbose
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Jan 15, 2026
1 parent e71b1ba commit bfb2e36
Showing 1 changed file with 40 additions and 25 deletions.
65 changes: 40 additions & 25 deletions local-app/python-tools/cross-organization/tag-checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__}")
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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()
Expand All @@ -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]

Expand All @@ -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():
Expand All @@ -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:
Expand Down

0 comments on commit bfb2e36

Please sign in to comment.