diff --git a/local-app/python-tools/cross-organization/tag-checker.py b/local-app/python-tools/cross-organization/tag-checker.py index 9fbd76a9..801c73fa 100755 --- a/local-app/python-tools/cross-organization/tag-checker.py +++ b/local-app/python-tools/cross-organization/tag-checker.py @@ -7,12 +7,13 @@ 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.7" +__version__ = "1.1.9" def get_args(): parser = argparse.ArgumentParser(description=f"AWS Org Tag Scanner v{__version__}") @@ -70,15 +71,20 @@ def scan_account(account, management_session, role_name, partition, tag_keys, re findings = [] global_resources = set() global_tags_found = set() - regional_data = [] + regional_metrics = [] - # Bar alignment fixed with bar_width + 1 - label = f"{acc_id} {alias}".ljust(bar_width + 1) + # UI Alignment: Fixed size title with lane ID padding + label = f"{acc_id} {alias}".ljust(bar_width) pbar = tqdm(total=len(tag_keys), desc=f"Lane {lane_id:02d} | {label}", position=lane_id, leave=False, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}') for key in tag_keys: for r in active_regions: + r_start = time.perf_counter() + r_hits = 0 + r_res_found = set() + r_tags_found = set() + client = m_session.client('resourcegroupstaggingapi', region_name=r) try: paginator = client.get_paginator('get_resources') @@ -92,25 +98,33 @@ def scan_account(account, management_session, role_name, partition, tag_keys, re }) global_resources.add(arn) global_tags_found.add(key) + r_res_found.add(arn) + r_tags_found.add(key) + r_hits += 1 except ClientError as e: if "Throttling" in str(e): time.sleep(1) + + r_elapsed = round(time.perf_counter() - r_start, 4) + r_entry = next((m for m in regional_metrics if m['region'] == r), None) + if not r_entry: + regional_metrics.append({ + "region": r, "hits": r_hits, "unique_resources": len(r_res_found), + "tags_found_count": len(r_tags_found), "tags_found_list": sorted(list(r_tags_found)), + "tags_not_found_count": len(tag_keys) - len(r_tags_found), "elapsed_sec": r_elapsed + }) + else: + r_entry['hits'] += r_hits + # Logic update for resource uniqueness within the region + current_tags = set(r_entry['tags_found_list']) | r_tags_found + r_entry['tags_found_list'] = sorted(list(current_tags)) + r_entry['tags_found_count'] = len(current_tags) + r_entry['tags_not_found_count'] = len(tag_keys) - len(current_tags) + r_entry['elapsed_sec'] = round(r_entry['elapsed_sec'] + r_elapsed, 4) + pbar.update(1) pbar.close() - for r in active_regions: - r_findings = [f for f in findings if f['region'] == r] - r_tags = sorted(list(set([f['tag_name'] for f in r_findings]))) - r_res = set([f['arn'] for f in r_findings]) - regional_data.append({ - "region": r, - "hits": len(r_findings), - "unique_resources": len(r_res), - "tags_found_count": len(r_tags), - "tags_found_list": r_tags, - "tags_not_found_count": len(tag_keys) - len(r_tags) - }) - metrics = { "global": { "hits": len(findings), @@ -120,14 +134,16 @@ def scan_account(account, management_session, role_name, partition, tag_keys, re "tags_not_found_count": len(tag_keys) - len(global_tags_found), "elapsed_sec": round(time.time() - acc_start, 2) }, - "regions": regional_data + "regions": regional_metrics } return findings, acc_id, alias, metrics, "Success" def main(): args = get_args() + cmd_line = " ".join(sys.argv) ts = datetime.now().strftime("%Y%m%d_%H%M%S") - start_overall = time.time() + start_iso = datetime.now().isoformat() + start_ts = time.time() session = boto3.Session(profile_name=args.profile, region_name=args.region) org = session.client('organizations', region_name=args.region) @@ -150,15 +166,13 @@ def main(): if args.limit > 0: all_accs = all_accs[:args.limit] - # Calculate bar width for alignment - # Length of ID (12) + Space (1) + Name/Alias (max) - max_label_len = max([12 + 1 + len(a['Name']) for a in all_accs]) if all_accs else 40 + # UI Alignment with +1 padding fix + max_label_len = max([12 + 1 + len(a['Name']) for a in all_accs]) + 1 if all_accs else 40 print(f"\n{'='*85}\nAWS TAG CHECKER v{__version__}\n{'='*85}") - print(f"Profile: {args.profile} | Workers: {args.max_workers} | Targets: {len(all_accs)}") all_findings = [] - summary_data = [] + account_results = [] overall_pbar = tqdm(total=len(all_accs), desc="Total Org Progress", position=0) with ThreadPoolExecutor(max_workers=args.max_workers) as executor: @@ -170,7 +184,7 @@ def main(): res, acc_id, alias, m, status = future.result() if status == "Success": all_findings.extend(res) - summary_data.append({ + account_results.append({ "account_id": acc_id, "alias": alias, "global_metrics": m["global"], "regional_metrics": m["regions"] @@ -182,17 +196,39 @@ def main(): overall_pbar.close() print("\n" * (args.max_workers + 1)) + # Final Summary Construction + total_hits = sum(a['global_metrics']['hits'] for a in account_results) + total_res = len(set(f['arn'] for f in all_findings)) + all_found_keys = set(f['tag_name'] for f in all_findings) + + output_summary = { + "summary": { + "version": __version__, + "command_line": cmd_line, + "aws_accounts_scanned": len(account_results), + "execution_start": start_iso, + "execution_end": datetime.now().isoformat(), + "elapsed_sec_total": round(time.time() - start_ts, 2), + "threads": args.max_workers, + "total_hits": total_hits, + "total_unique_resources": total_res, + "total_tags_found_count": len(all_found_keys), + "total_tags_not_found_count": len(tag_keys) - len(all_found_keys) + }, + "accounts": account_results + } + sum_file = f"{args.output}_summary_{ts}.json" fin_file = f"{args.output}_findings_{ts}.csv" - with open(sum_file, 'w') as f: json.dump(summary_data, f, indent=4) + with open(sum_file, 'w') as f: json.dump(output_summary, f, indent=4) if all_findings: with open(fin_file, 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=all_findings[0].keys()) writer.writeheader(); writer.writerows(all_findings) - print(f"[+] Scan Complete in {round(time.time()-start_overall, 2)}s") - print(f"[+] Summary: {sum_file} | Findings: {fin_file}") + print(f"[+] Summary: {sum_file}") + print(f"[+] Findings: {fin_file}") if __name__ == "__main__": main()