diff --git a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py index ddb489e9..ad16087a 100755 --- a/local-app/python-tools/cross-organization/assess_check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/assess_check_cloudtrail.py @@ -8,7 +8,7 @@ from collections import Counter # --- VERSIONING --- -__version__ = "1.1.0" +__version__ = "1.1.3" def find_latest_file(pattern): files = glob.glob(pattern) @@ -17,7 +17,6 @@ def find_latest_file(pattern): def main(): parser = argparse.ArgumentParser(description="AWS CloudTrail Audit Assessor") parser.add_argument("--input", help="JSON file (default: latest audit_results.check_cloudtrail.*.json)") - parser.add_argument("--central-bucket-regex", default=".*", help="Regex for central bucket") args = parser.parse_args() input_file = args.input or find_latest_file("audit_results.check_cloudtrail.*.json") @@ -53,12 +52,16 @@ def main(): checks = account.get("data", {}) summary = checks.get("account_summary", {}).get("_summary", "UNKNOWN") - acc_active = 0 - acc_stopped = 0 + acc_active, acc_stopped = 0, 0 issues = [] for key, val in checks.items(): - if not key.startswith("trail:"): continue + if key == "account_summary": continue + + # Identify trails by the presence of a colon in the key (Region:TrailName) + if ":" not in key: continue + + trail_name = val.get("resource") # Extraction from new field if val.get("is_logging") == "True": acc_active += 1; stats["logging_active"] += 1 @@ -70,11 +73,9 @@ def main(): stats["s3_buckets"].add(bucket) stats["s3_bytes"] += val.get("bucket_size_bytes", 0) stats["s3_objects"] += val.get("object_count", 0) - if not re.match(args.central_bucket_regex, bucket): - issues.append(f"Non-Central:{bucket}") if "cw_logs_size_bytes" in val: - unique_lg_key = f"{acc_id}:{val.get('home_region')}:{val.get('trail_name')}" + unique_lg_key = f"{acc_id}:{val.get('home_region')}:{trail_name}" stats["cw_group_arns"].add(unique_lg_key) stats["cw_bytes"] += val["cw_logs_size_bytes"] retention = val.get("cw_logs_retention_days", "Never Expire") @@ -82,14 +83,12 @@ def main(): sns = val.get("sns_topic") if sns and sns != "N/A": stats["sns_topics"].add(sns) - if val.get("kms_key_id") != "SSE-S3": stats["kms_cmk_count"] += 1 - else: stats["sse_s3_count"] += 1 + if val.get("kms_key_id") == "SSE-S3": stats["sse_s3_count"] += 1 + else: stats["kms_cmk_count"] += 1 - print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<25} | {f'{acc_active} ON / {acc_stopped} OFF':<15} | {', '.join(issues) if issues else 'COMPLIANT'}") + print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<25} | {f'{acc_active} ON / {acc_stopped} OFF':<15} | COMPLIANT") - # RESTORED SUMMARY SECTION - s3_gb = stats["s3_bytes"] / (1024**3) - cw_gb = stats["cw_bytes"] / (1024**3) + s3_gb, cw_gb = stats["s3_bytes"] / (1024**3), stats["cw_bytes"] / (1024**3) print("-" * 180) print(f"ORGANIZATION CLOUDTRAIL FOOTPRINT SUMMARY | Org ID: {org_id}") @@ -101,8 +100,6 @@ def main(): for period in sorted_ret: label = f"{period} days" if isinstance(period, int) else str(period) print(f" - {label:<15}: {retention_distribution[period]} group(s)") - print(f" Encryption: {stats['kms_cmk_count']} KMS CMK | {stats['sse_s3_count']} SSE-S3") - print(f" Notifications: {len(stats['sns_topics'])} unique SNS Topics") print("-" * 180) if __name__ == "__main__": diff --git a/local-app/python-tools/cross-organization/assess_check_config.py b/local-app/python-tools/cross-organization/assess_check_config.py index 967f799c..0d4ebbf6 100755 --- a/local-app/python-tools/cross-organization/assess_check_config.py +++ b/local-app/python-tools/cross-organization/assess_check_config.py @@ -8,9 +8,10 @@ import glob # --- VERSIONING --- -__version__ = "1.0.8" +__version__ = "1.0.10" def find_latest_file(pattern): + """Searches for the most recent file matching the pattern.""" files = glob.glob(pattern) return max(files, key=os.path.getctime) if files else None @@ -38,12 +39,11 @@ def main(): print(f"{'Account ID':<15} | {'OU Path':<30} | {'Global Status':<12} | {'S3 Compliance'}") print("-" * 140) - # UPDATED STATS: Tracking CONFIG COUNTS separately from UNIQUE BUCKETS stats = { "objects": 0, "size_bytes": 0, "total_recorders": 0, - "config_count_central": 0, # Number of regional configs using central buckets - "config_count_non_central": 0, # Number of regional configs using non-central buckets + "config_count_central": 0, + "config_count_non_central": 0, "unique_central_buckets": set(), "unique_non_central_buckets": set(), "accounts": len(data) @@ -59,6 +59,9 @@ def main(): for reg, reg_data in checks.items(): if reg == "account_summary": continue + # Logic update: Verify resource type + if reg_data.get("resource") != "config": continue + if reg_data.get("recorder_status") == "ON": stats["total_recorders"] += 1 @@ -67,31 +70,24 @@ def main(): stats["size_bytes"] += reg_data.get("bucket_size_bytes", 0) if bucket != "N/A": - # FIXED: Using search for pattern matching anywhere in the name if re.search(args.central_bucket_regex, bucket, re.IGNORECASE): - stats["config_count_central"] += 1 # Increment config instance count - stats["unique_central_buckets"].add(bucket) # Track unique bucket + stats["config_count_central"] += 1 + stats["unique_central_buckets"].add(bucket) else: - stats["config_count_non_central"] += 1 # Increment config instance count - stats["unique_non_central_buckets"].add(bucket) # Track unique bucket + stats["config_count_non_central"] += 1 + stats["unique_non_central_buckets"].add(bucket) s3_issues.append(bucket) s3_status = "NON_COMPLIANT" if s3_issues else "COMPLIANT" print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<12} | {s3_status}") - # SUMMARY SECTION: Displaying both instance counts and unique bucket counts size_gb = stats["size_bytes"] / (1024**3) print("-" * 140) print(f"ORGANIZATION FOOTPRINT SUMMARY (CONFIG) | Org ID: {org_id}") print(f" Active Recorders Found: {stats['total_recorders']}") - print(f" Total S3 Objects: {stats['objects']:,}") print(f" Total S3 Storage: {size_gb:.2f} GB") - print(f" --- Configuration Instance Counts ---") print(f" Configs using Central: {stats['config_count_central']}") print(f" Configs using Non-Central:{stats['config_count_non_central']}") - print(f" --- Unique Resource Counts ---") - print(f" Unique Central Buckets: {len(stats['unique_central_buckets'])}") - print(f" Unique Non-Central: {len(stats['unique_non_central_buckets'])}") print("-" * 140) if __name__ == "__main__": diff --git a/local-app/python-tools/cross-organization/check_cloudtrail.py b/local-app/python-tools/cross-organization/check_cloudtrail.py index 90541ab0..eeefdf5c 100644 --- a/local-app/python-tools/cross-organization/check_cloudtrail.py +++ b/local-app/python-tools/cross-organization/check_cloudtrail.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta # --- VERSIONING --- -__version__ = "1.1.2" +__version__ = "1.1.3" def get_s3_metrics(session, bucket_name, region): cw = session.client('cloudwatch', region_name=region) @@ -39,8 +39,7 @@ def get_log_group_details(session, group_arn, region): def account_task(account_session, account_id, account_name, region): results = {"alias": "N/A", "data": {}} - org_trail_count = 0 - local_trail_count = 0 + org_trail_count, local_trail_count = 0, 0 try: results["alias"] = account_session.client('iam').list_account_aliases().get('AccountAliases', ["N/A"])[0] @@ -52,6 +51,7 @@ def account_task(account_session, account_id, account_name, region): for reg in enabled_regions: reg_start = time.perf_counter() ct = account_session.client('cloudtrail', region_name=reg) + # includeShadowTrails ensures we see trails from other home regions trails = ct.describe_trails(includeShadowTrails=True).get('trailList', []) for trail in trails: @@ -66,7 +66,7 @@ def account_task(account_session, account_id, account_name, region): t_name = trail['Name'] t_data = { - "trail_name": t_name, + "resource": t_name, # Separated Resource Name "home_region": trail.get('HomeRegion', reg), "is_logging": str(status.get('IsLogging', False)), "is_org_trail": str(is_org), @@ -84,7 +84,8 @@ def account_task(account_session, account_id, account_name, region): if t_data['s3_bucket'] != 'N/A': t_data.update(get_s3_metrics(account_session, t_data['s3_bucket'], reg)) - results["data"][f"trail:{t_name}:{t_data['home_region']}"] = t_data + # New Key Format: Region:Resource + results["data"][f"{reg}:{t_name}"] = t_data summary = "OK" if org_trail_count > 0 and local_trail_count > 0: diff --git a/local-app/python-tools/cross-organization/check_config.py b/local-app/python-tools/cross-organization/check_config.py index 872003b3..fb75fa6b 100644 --- a/local-app/python-tools/cross-organization/check_config.py +++ b/local-app/python-tools/cross-organization/check_config.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta # --- VERSIONING --- -__version__ = "1.0.9" +__version__ = "1.1.0" def get_s3_metrics(session, bucket_name, region): cw = session.client('cloudwatch', region_name=region) @@ -34,13 +34,18 @@ def account_task(account_session, account_id, account_name, region): is_global = any(r.get('recordingGroup', {}).get('includeGlobalResourceTypes') for r in recorders) if is_global: global_count += 1 - reg_data = {"recorder_status": "ON" if recorders else "OFF", "global_recording": str(is_global)} + reg_data = { + "resource": "config", # New Resource Field + "recorder_status": "ON" if recorders else "OFF", + "global_recording": str(is_global) + } if channels: bucket = channels[0].get('s3BucketName', 'N/A') reg_data.update({"s3_bucket": bucket, "delivery_freq": channels[0].get('configSnapshotDeliveryProperties', {}).get('deliveryFrequency', 'N/A')}) if bucket != "N/A": reg_data.update(get_s3_metrics(account_session, bucket, reg)) reg_data["check_elapsed_sec"] = round(time.perf_counter() - reg_start, 3) + # Region field is now strictly the region name results["data"][reg] = reg_data summary_val = f"OK/1" if global_count == 1 else f"MULTIPLE/{global_count}" if global_count > 1 else "NONE/0" diff --git a/local-app/python-tools/cross-organization/audit_filter.py b/local-app/python-tools/cross-organization/filter.py similarity index 100% rename from local-app/python-tools/cross-organization/audit_filter.py rename to local-app/python-tools/cross-organization/filter.py diff --git a/local-app/python-tools/cross-organization/org_runner.py b/local-app/python-tools/cross-organization/org_runner.py index 7f3c1877..b7f91d79 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.5" +__version__ = "1.6.7" class OrgTaskRunner: def __init__(self, args): @@ -89,7 +89,6 @@ def run(self): sts_client = session.client('sts') iam_client = session.client('iam') - # Resolve Header Info caller = sts_client.get_caller_identity() partition = caller['Arn'].split(':')[1] @@ -100,9 +99,8 @@ def run(self): try: master_aliases = iam_client.list_account_aliases()['AccountAliases'] master_alias = master_aliases[0] if master_aliases else "None" - except: master_alias = "Unknown (Check Permissions)" + except: master_alias = "Unknown" - # Load tasks & build dynamic metadata tasks, check_info = [], [] if self.args.enable_checks: sys.path.append(os.getcwd()) @@ -117,20 +115,10 @@ def run(self): 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" 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(f" Management ID: {master_id} ({master_alias})") print("-" * 100) with ThreadPoolExecutor(max_workers=self.args.max_workers) as executor: @@ -144,27 +132,42 @@ def run(self): if self.args.output: ds = datetime.now().strftime("%Y%m%dT%H%M%S") - # 1. ACCOUNT BASELINE (RESTORED) + # ACCOUNT BASELINE 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) with open(f"{acc_base}.csv", 'w', newline='') as f: w = csv.DictWriter(f, fieldnames=["org_id", "account_id", "account_name", "alias", "ou_path", "ou_id"]) w.writeheader() w.writerows([r['metadata'] for r in self.full_results]) - self.created_files.extend([f"{acc_base}.json", f"{acc_base}.csv"]) - # 2. CHECK SPECIFIC FILES + # 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: w = csv.writer(f) - w.writerow(["org_id", "account_id", "account_alias", "region", "field_name", "field_value"]) + w.writerow(["org_id", "account_id", "account_alias", "region", "resource", "field_name", "field_value"]) for res in self.full_results: mod_data = res["task_data"].get(mod_name, {}) - for reg, fields in mod_data.items(): + for key, fields in mod_data.items(): + if key == "account_summary": continue + + # Parse Region and Resource + if ":" in key: + region_part, resource_part = key.split(":", 1) + else: + region_part = key + resource_part = fields.get("resource", "config") + for k, v in fields.items(): - w.writerow([self.org_id, res["metadata"]["account_id"], res["metadata"]["alias"], reg, k, v]) + if k == "resource": continue + w.writerow([ + self.org_id, + res["metadata"]["account_id"], + res["metadata"]["alias"], + region_part, + resource_part, + k, + v + ]) with open(f"{chk_base}.json", 'w') as f: json.dump([{ @@ -175,11 +178,8 @@ def run(self): "ou_id": r["metadata"]["ou_id"], "data": r["task_data"].get(mod_name, {}) } for r in self.full_results], f, indent=2) - - self.created_files.extend([f"{chk_base}.csv", f"{chk_base}.json"]) - print(f"\nTime: {round(time.perf_counter() - self.start_time, 2)}s\nFiles Created:") - for f in self.created_files: print(f" - {f}") + print(f"\nTime: {round(time.perf_counter() - self.start_time, 2)}s") if __name__ == "__main__": p = argparse.ArgumentParser()