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 548a07c4..11d4cf04 100755 --- a/local-app/python-tools/cross-organization/assess_check_config.py +++ b/local-app/python-tools/cross-organization/assess_check_config.py @@ -3,28 +3,72 @@ import json import argparse import re +import sys -__version__ = "1.0.0" +# --- VERSIONING --- +__version__ = "1.0.1" def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--input", required=True, help="The JSON audit file") - parser.add_argument("--central-bucket-regex", required=True, help="Regex for central bucket") + parser = argparse.ArgumentParser(description=f"AWS Config Audit Assessor v{__version__}") + parser.add_argument("--input", required=True, help="The JSON audit file from check_config") + parser.add_argument("--central-bucket-regex", required=True, help="Regex to validate corporate S3 standards") args = parser.parse_args() - with open(args.input, 'r') as f: - data = json.load(f) + try: + with open(args.input, 'r') as f: + data = json.load(f) + except Exception as e: + print(f"Error reading input file: {e}") + sys.exit(1) - print(f"{'Account ID':<15} | {'Region':<15} | {'Match':<6} | {'Bucket Name'}") - print("-" * 70) + print("-" * 110) + print(f"{'Account ID':<15} | {'Alias':<20} | {'Global Status':<12} | {'S3 Compliance'}") + print("-" * 110) - for acc in data: - for reg, checks in acc["checks"].items(): - bucket = checks.get("s3_bucket", "N/A") + total_accounts = len(data) + non_compliant_s3 = 0 + duplicate_globals = 0 + + for account in data: + acc_id = account.get("account_id", "Unknown") + alias = account.get("alias", "N/A") + checks = account.get("data", {}) + + # 1. Evaluate Global Recording Summary + # Look for the 'account_summary' key injected by check_config.py + summary_record = checks.get("account_summary", {}) + global_status = summary_record.get("_summary", "MISSING") + + if "MULTIPLE" in global_status: + duplicate_globals += 1 + + # 2. Evaluate S3 Regional Compliance + s3_issues = [] + for reg, reg_data in checks.items(): + if reg == "account_summary": + continue + + bucket = reg_data.get("s3_bucket", "N/A") if bucket != "N/A": - match = bool(re.match(args.central_bucket_regex, bucket)) - if not match: - print(f"{acc['account_id']:<15} | {reg:<15} | {str(match):<6} | {bucket}") + is_valid = bool(re.match(args.central_bucket_regex, bucket)) + if not is_valid: + s3_issues.append(f"{reg}:{bucket}") + + if s3_issues: + non_compliant_s3 += 1 + s3_str = ", ".join(s3_issues) + else: + s3_str = "✅ Compliant" + + print(f"{acc_id:<15} | {alias[:20]:<20} | {global_status:<12} | {s3_str}") + + # Final Summary Footer + print("-" * 110) + print(f"ASSESSMENT SUMMARY:") + print(f" Total Accounts Scanned: {total_accounts}") + print(f" Accounts with Duplicate Global Recording: {duplicate_globals}") + print(f" Accounts with Non-Compliant S3 Buckets: {non_compliant_s3}") + print("-" * 110) if __name__ == "__main__": main() diff --git a/local-app/python-tools/cross-organization/check_config.py b/local-app/python-tools/cross-organization/check_config.py index 478b8c13..e20bb10c 100644 --- a/local-app/python-tools/cross-organization/check_config.py +++ b/local-app/python-tools/cross-organization/check_config.py @@ -2,7 +2,7 @@ import time from datetime import datetime, timedelta -__version__ = "1.0.7" +__version__ = "1.0.8" def get_s3_metrics(session, bucket_name, region): cw = session.client('cloudwatch', region_name=region) @@ -30,6 +30,7 @@ def account_task(account_session, account_id, account_name, region): recorders = config.describe_configuration_recorders().get('ConfigurationRecorders', []) channels = config.describe_delivery_channels().get('DeliveryChannels', []) + # Re-added Global Resource Check is_global = any(r.get('recordingGroup', {}).get('includeGlobalResourceTypes') for r in recorders) if is_global: global_count += 1 @@ -42,7 +43,7 @@ def account_task(account_session, account_id, account_name, region): reg_data["check_elapsed_sec"] = round(time.perf_counter() - reg_start, 3) results["data"][reg] = reg_data - # Add Per-Account Summary record (using 'global' as the region key for the CSV row) + # Per-Account Summary record summary_val = f"OK/1" if global_count == 1 else f"MULTIPLE/{global_count}" if global_count > 1 else "NONE/0" results["data"]["account_summary"] = {"_summary": summary_val} diff --git a/local-app/python-tools/cross-organization/org_runner.py b/local-app/python-tools/cross-organization/org_runner.py index 2bf2ef59..4ef6fafb 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.0" +__version__ = "1.6.1" class OrgTaskRunner: def __init__(self, args): @@ -28,6 +28,9 @@ def __init__(self, args): self.created_files = [] self.start_time = 0 + def log_print(self, message=""): + self.output_log.append(message) + def get_ou_path(self, org_client, entity_id): if entity_id in self.hierarchy_cache: return self.hierarchy_cache[entity_id] if entity_id.startswith('r-'): @@ -36,7 +39,8 @@ def get_ou_path(self, org_client, entity_id): try: parents = org_client.list_parents(ChildId=entity_id)['Parents'] p_id = parents[0]['Id'] if parents else None - ou_name = org_client.describe_organizational_unit(OrganizationalUnitId=entity_id)['OrganizationalUnit']['Name'] + ou_desc = org_client.describe_organizational_unit(OrganizationalUnitId=entity_id) + ou_name = ou_desc['OrganizationalUnit']['Name'] p_path, _ = self.get_ou_path(org_client, p_id) if p_id else (None, None) path = f"{p_path}:{ou_name}" if p_path else ou_name self.hierarchy_cache[entity_id] = (path, entity_id) @@ -53,11 +57,13 @@ def process_account(self, acc, partition, tasks): ou_path, ou_id = self.get_ou_path(org, parents[0]['Id']) if parents else ("Orphaned", "N/A") ou_path = ou_path if ou_path else "Root" - account_data = { + account_metadata = { "account_id": acc_id, "account_name": acc_name, "alias": "N/A", - "ou_path": ou_path, "ou_id": ou_id, "checks": {} + "ou_path": ou_path, "ou_id": ou_id } + account_results = {"metadata": account_metadata, "checks": {}} + try: assumed = sts.assume_role(RoleArn=role_arn, RoleSessionName="OrgRunner") m_sess = boto3.Session( @@ -68,9 +74,9 @@ def process_account(self, acc, partition, tasks): ) for t_func in tasks: res = t_func(m_sess, acc_id, acc_name, self.args.region) - account_data["alias"] = res.get("alias", "N/A") - account_data["checks"].update(res.get("data", {})) - return account_data, None + account_results["metadata"]["alias"] = res.get("alias", "N/A") + account_results["checks"].update(res.get("data", {})) + return account_results, None except Exception as e: return None, f"FAILED {acc_name}: {str(e)}" @@ -104,26 +110,31 @@ def run(self): if self.args.output: ds = datetime.now().strftime("%Y%m%dT%H%M%S") - # 1. ACCOUNT BASELINE FILES + # 1. ACCOUNT BASELINE (Metadata Only) acc_base = f"audit_results.account.{ds}" - with open(f"{acc_base}.json", 'w') as f: json.dump(self.full_results, f, indent=2) + 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=["account_id", "account_name", "alias", "ou_path", "ou_id"]) w.writeheader() - w.writerows([{k: v for k, v in acc.items() if k != 'checks'} for acc in self.full_results]) + 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 + # 2. CHECK SPECIFIC FILES (Data + Account Context) for cn in check_names: chk_base = f"audit_results.{cn}.{ds}" + # Save CSV (Long Format) with open(f"{chk_base}.csv", 'w', newline='') as f: w = csv.writer(f) - w.writerow(["account_id", "region", "field_name", "field_value"]) - for acc in self.full_results: - for reg, fields in acc["checks"].items(): + w.writerow(["account_id", "account_alias", "region", "field_name", "field_value"]) + for res in self.full_results: + for reg, fields in res["checks"].items(): for k, v in fields.items(): - w.writerow([acc["account_id"], reg, k, v]) - self.created_files.append(f"{chk_base}.csv") + w.writerow([res["metadata"]["account_id"], res["metadata"]["alias"], reg, k, v]) + # Save JSON (Hierarchical) + with open(f"{chk_base}.json", 'w') as f: + json.dump([{"account_id": r["metadata"]["account_id"], "alias": r["metadata"]["alias"], "data": r["checks"]} 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}")