Output: ./2026-02-12.md: "output": "#!/usr/bin/env python3\nimport boto3\nimport csv\nimport time\nfrom botocore.exceptions import ClientError, ProfileNotFound\nfrom collections import defaultdict\n\n# The original script's helper function to get an AMI's human-readable name from its ID\ndef get_ami_name(ec2_client, ami_id):\n \"\"\"\n Fetches the AMI Name from the AMI ID using the EC2 client.\n Handles potential errors if the AMI is not accessible or deleted.\n \"\"\"\n if not ami_id:\n return \"N/A - No AMI ID\"\n # Skip AMI lookup if the ID is clearly an error message from a previous step\n if not ami_id.startswith('ami-'):\n return f\"Invalid AMI ID format\"\n \n try:\n # Use describe_images to get metadata about the AMI\n image_info = ec2_client.describe_images(ImageIds=[ami_id])['Images']\n if image_info:\n return image_info[0].get('Name', 'Unnamed AMI')\n return 'AMI not found'\n except ClientError as e:\n # Handle cases where the AMI is not found or permissions are denied\n return f\"AMI not accessible (Error: {e.response['Error']['Code']})\"\n except Exception as e:\n return f\"AMI lookup error: {e}\"\n\n\ndef get_asg_details(account_id, account_name, region, asg_client, ec2_client):\n \"\"\"\n Lists all Auto Scaling Groups (ASGs) in the region and determines\n their associated AMI ID, Launch Configuration, or Launch Template details.\n \"\"\"\n asg_info_list = []\n paginator = asg_client.get_paginator('describe_auto_scaling_groups')\n\n print(f\" -> Fetching ASGs in {region}...\")\n\n try:\n for page in paginator.paginate():\n for asg in page['AutoScalingGroups']:\n ami_id = \"N/A\"\n template_type = \"None\"\n template_name = \"None\"\n template_version = \"N/A\"\n\n # 1. Check for Launch Template\n lt_data = asg.get('LaunchTemplate') or asg.get('MixedInstancesPolicy', {}).get('LaunchTemplate')\n \n if lt_data:\n # An ASG can use either a primary LaunchTemplate or a MixedInstancesPolicy\n template_type = \"LaunchTemplate\"\n template_id = lt_data.get('LaunchTemplateId')\n template_name = lt_data.get('LaunchTemplateName')\n template_version = lt_data.get('Version') or \"$Default\" # Use $Default if version is missing\n \n if template_id or template_name:\n # Attempt to resolve the AMI from the Launch Template version\n try:\n # AWS API requires EITHER LaunchTemplateId OR LaunchTemplateName, not both.\n lt_lookup_args = {'Versions': [template_version]}\n \n if template_id:\n lt_lookup_args['LaunchTemplateId'] = template_id\n elif template_name:\n lt_lookup_args['LaunchTemplateName'] = template_name\n else:\n # This case is unlikely if lt_data was present, but provides robust error handling\n raise ValueError(\"Launch Template ID and Name are both missing.\")\n \n lt_versions = ec2_client.describe_launch_template_versions(**lt_lookup_args)['LaunchTemplateVersions']\n\n if lt_versions:\n # AMI ID is nested inside LaunchTemplateData\n launch_template_data = lt_versions[0]['LaunchTemplateData']\n ami_id = launch_template_data.get('ImageId', 'AMI ID not specified in LT')\n \n except ClientError as e:\n # Capture the specific error code for debugging\n ami_id = f\"LT lookup error: {e.response['Error']['Code']}\"\n except Exception as e:\n ami_id = f\"LT processing error: {e}\"\n\n # 2. Check for Launch Configuration (Legacy)\n elif asg.get('LaunchConfigurationName'):\n lc_name = asg['LaunchConfigurationName']\n template_type = \"LaunchConfiguration\"\n template_name = lc_name\n template_version = \"Latest\" # LCs don't have versions like LTs\n\n # Attempt to resolve the AMI from the Launch Configuration\n try:\n lc_response = asg_client.describe_launch_configurations(\n LaunchConfigurationNames=[lc_name]\n )['LaunchConfigurations']\n \n if lc_response:\n ami_id = lc_response[0].get('ImageId', 'AMI ID not specified in LC')\n \n except ClientError as e:\n ami_id = f\"LC lookup error: {e.response['Error']['Code']}\"\n except Exception as e:\n ami_id = f\"LC processing error: {e}\"\n \n # Resolve the AMI Name if an ID was found\n ami_name = get_ami_name(ec2_client, ami_id)\n \n asg_info_list.append({\n 'AccountName': account_name,\n 'AccountID': account_id,\n 'Region': region,\n 'ASG_Name': asg['AutoScalingGroupName'],\n 'Template_Type': template_type,\n 'Template_Name': template_name,\n 'Template_Version': template_version,\n 'AMI_ID': ami_id,\n 'AMI_Name': ami_name,\n })\n \n except ClientError as e:\n print(f\" -> Error listing ASGs in {region}: {e}\")\n \n return asg_info_list\n\n# Initialize AWS Organizations client\n# NOTE: This part is taken directly from your example script's organizational structure.\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs) to check\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] \nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nprint(\"1. Fetching active accounts from specified OUs...\")\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\nprint(f\"-> Found {len(active_accounts)} active accounts to check.\")\n\nall_asg_info = []\n\n# Define the regions to check (matches your example script)\nregions_to_check = ['eu-west-1', 'eu-west-2'] \n\n# Open CSV file for writing ASG information\ncsv_filepath = 'autoscaling_group_ami_template_info.csv'\nwith open(csv_filepath, mode='w', newline='') as csvfile:\n fieldnames = [\n 'AccountName', \n 'AccountID', \n 'Region', \n 'ASG_Name', \n 'Template_Type', \n 'Template_Name', \n 'Template_Version', \n 'AMI_ID', \n 'AMI_Name'\n ]\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n print(\"\\n2. Iterating through accounts and regions to collect ASG data...\")\n for account in active_accounts:\n account_id = account['Id']\n # Sanitize account name for profile use (matching original script's logic)\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' \n \n print(f\"\\n--- Checking Account: {account_name} (ID: {account_id}) ---\")\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n # NOTE: This relies on your local AWS configuration having these profiles set up.\n boto3.setup_default_session(profile_name=profile_name)\n \n # Loop through each region\n for region in regions_to_check:\n try:\n # Initialize clients for the specific region\n asg_client = boto3.client('autoscaling', region_name=region)\n ec2_client = boto3.client('ec2', region_name=region) # Needed to resolve AMI Name and LT details\n \n # Get ASG information\n asg_data = get_asg_details(account_id, account_name, region, asg_client, ec2_client)\n \n # Write results to CSV and print to console\n for asg in asg_data:\n all_asg_info.append(asg)\n writer.writerow(asg)\n print(f\" [FOUND] ASG: {asg['ASG_Name']}, Template Type: {asg['Template_Type']}, Template Name: {asg['Template_Name']} ({asg['Template_Version']}), AMI ID: {asg['AMI_ID']}, AMI Name: {asg['AMI_Name']}\")\n\n except (ClientError) as e:\n print(f\" -> Error accessing services in region '{region}': {e}\")\n \n # Small delay to respect API limits, especially for org-wide searches\n time.sleep(0.5)\n \n except (ClientError, ProfileNotFound) as e:\n print(f\" -> Error setting up session/profile '{profile_name}': {e}\")\n\n\n# Output summary\ntotal_asgs = len(all_asg_info)\nprint(\"\\n========================================================\")\nprint(\"ASG & AMI Collection Summary:\")\nprint(f\"Total Auto Scaling Groups found: {total_asgs}\")\n\nif total_asgs > 0:\n # Basic summary of AMI usage\n ami_usage = defaultdict(int)\n for asg in all_asg_info:\n ami_usage[asg['AMI_Name']] += 1\n \n print(f\"Distinct AMI Names used: {len(ami_usage)}\")\n print(\"\\nTop 5 AMI Usage Counts:\")\n for ami_name, count in sorted(ami_usage.items(), key=lambda item: item[1], reverse=True)[:5]:\n print(f\" - {ami_name}: {count} ASGs\")\n\nprint(f\"\\nCSV file '{csv_filepath}' generated successfully.\")\nprint(\"========================================================\")\n" ./2026-02-12.md: "output": "import boto3\nimport csv\nfrom botocore.exceptions import ClientError, ProfileNotFound\n\n# Function to get the VPC name from tags\ndef get_vpc_name(tags):\n \"\"\"\n Retrieves the value of the 'Name' tag from a VPC's tags.\n Returns \"Unnamed VPC\" if the tag is not found.\n \"\"\"\n if tags:\n for tag in tags:\n if tag['Key'] == 'Name':\n return tag['Value']\n return \"Unnamed VPC\"\n\n# Function to list all VPCs with name, ID, and CIDR block\ndef list_vpcs(ec2_client):\n \"\"\"\n Lists all VPCs in a given region, retrieves their Name, ID,\n and CIDR block, and returns the information in a list of dictionaries.\n \"\"\"\n vpcs_info = []\n paginator = ec2_client.get_paginator('describe_vpcs')\n \n for page in paginator.paginate():\n for vpc in page['Vpcs']:\n vpc_name = get_vpc_name(vpc.get('Tags'))\n vpc_id = vpc['VpcId']\n cidr_block = vpc.get('CidrBlock', 'N/A')\n \n vpcs_info.append({\n 'VPCName': vpc_name,\n 'VPCID': vpc_id,\n 'CIDRBlock': cidr_block\n })\n return vpcs_info\n\n# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Define the regions to check\nregions_to_check = ['eu-west-1', 'eu-west-2']\n\n# Initialize a counter for the total number of VPCs\ntotal_vpcs_all_accounts = 0\n\n# Open CSV file for writing VPC information\nwith open('aws_vpcs_by_region.csv', mode='w', newline='') as csvfile:\n # Define the fieldnames for the CSV header\n fieldnames = ['AccountName', 'AccountID', 'Region', 'VPCName', 'VPCID', 'CIDRBlock']\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n for account in active_accounts:\n account_id = account['Id']\n \n # Replace \" - \" with \"-\" and replace spaces with \"-\" in account names\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' # Constructing the profile name\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n boto3.setup_default_session(profile_name=profile_name)\n \n for region in regions_to_check:\n try:\n # Initialize EC2 client for the specific region\n ec2_client = boto3.client('ec2', region_name=region)\n \n # List all VPCs for the account and region\n vpcs_info = list_vpcs(ec2_client)\n \n # Add the count for this region to the total\n total_vpcs_all_accounts += len(vpcs_info)\n \n # Write VPC information to CSV\n for vpc in vpcs_info:\n writer.writerow({\n 'AccountName': account_name,\n 'AccountID': account_id,\n 'Region': region,\n 'VPCName': vpc['VPCName'],\n 'VPCID': vpc['VPCID'],\n 'CIDRBlock': vpc['CIDRBlock']\n })\n \n # Print the results for the current account and region\n print(f'\"{account_name}\" (ID: {account_id}, Region: {region}) VPCs:')\n for vpc in vpcs_info:\n print(f\"VPC Name: {vpc['VPCName']}, VPC ID: {vpc['VPCID']}, CIDR Block: {vpc['CIDRBlock']}\")\n print('--------------------------------------------------------')\n\n except ClientError as e:\n print(f\"Error accessing VPCs in region '{region}' for account '{account_name}' (ID: {account_id}): {e}\")\n\n except (ClientError, ProfileNotFound) as e:\n print(f\"Error accessing account '{account_name}' (ID: {account_id}): {e}\")\n\nprint(\"\\nSummary of VPCs Across All Accounts and Regions:\")\nprint(f\"Total number of VPCs found: {total_vpcs_all_accounts}\")\nprint(\"\\nCSV file 'aws_vpcs_by_region.csv' generated successfully.\")\n" ./2026-02-12.md: "output": "import boto3\nfrom botocore.exceptions import ClientError, ProfileNotFound\n\n# Function to list RDS instances\ndef list_rds_instances(rds_client):\n instance_names = []\n paginator = rds_client.get_paginator('describe_db_instances')\n for page in paginator.paginate():\n for db_instance in page['DBInstances']:\n instance_names.append(db_instance['DBInstanceIdentifier'])\n return instance_names\n\n# Function to list RDS clusters\ndef list_rds_clusters(rds_client):\n cluster_names = []\n paginator = rds_client.get_paginator('describe_db_clusters')\n for page in paginator.paginate():\n for db_cluster in page['DBClusters']:\n cluster_names.append(db_cluster['DBClusterIdentifier'])\n return cluster_names\n\n# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Iterate through active accounts and print RDS names\nfor account in active_accounts:\n account_id = account['Id']\n \n # Replace \" - \" with \"-\" and replace spaces with \"-\" in account names\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' # Constructing the profile name\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n boto3.setup_default_session(profile_name=profile_name)\n \n # Initialize RDS client with the SSO session\n rds_client = boto3.client('rds')\n \n # Get RDS instance names\n rds_instance_names = list_rds_instances(rds_client)\n \n # Get RDS cluster names\n rds_cluster_names = list_rds_clusters(rds_client)\n \n # Print RDS instance names\n for rds_name in rds_instance_names:\n print(f'\"{account_name}\":\"{rds_name}\"')\n \n # Print RDS cluster names\n for rds_name in rds_cluster_names:\n print(f'\"{account_name}\":\"{rds_name}\"')\n \n print(f\"Processed account '{account_name}' (ID: {account_id})\")\n\n except (ClientError, ProfileNotFound) as e:\n print(f\"Error accessing account '{account_name}' (ID: {account_id}): {e}\")\n\nprint(\"\\nRDS names have been successfully listed for all accounts.\")\n" ./2026-02-12.md: "content": "import boto3\nimport csv\nfrom botocore.exceptions import ClientError, ProfileNotFound\nfrom aws_utils import setup_org_accounts_session\n\ndef get_vpc_name(tags):\n if tags:\n for tag in tags:\n if tag['Key'] == 'Name':\n return tag['Value']\n return \"Unnamed VPC\"\n\ndef list_vpcs(ec2_client):\n vpcs_info = []\n paginator = ec2_client.get_paginator('describe_vpcs')\n for page in paginator.paginate():\n for vpc in page['Vpcs']:\n vpcs_info.append({\n 'VPCName': get_vpc_name(vpc.get('Tags')),\n 'VPCID': vpc['VpcId'],\n 'CIDRBlock': vpc.get('CidrBlock', 'N/A')\n })\n return vpcs_info\n\ndef main():\n ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n regions_to_check = ['eu-west-1', 'eu-west-2']\n total_vpcs_all_accounts = 0\n\n with open('aws_vpcs_by_region.csv', mode='w', newline='') as csvfile:\n fieldnames = ['AccountName', 'AccountID', 'Region', 'VPCName', 'VPCID', 'CIDRBlock']\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n for account_id, account_name, profile_name in setup_org_accounts_session(ou_ids):\n try:\n boto3.setup_default_session(profile_name=profile_name)\n for region in regions_to_check:\n try:\n ec2_client = boto3.client('ec2', region_name=region)\n vpcs_info = list_vpcs(ec2_client)\n total_vpcs_all_accounts += len(vpcs_info)\n \n for vpc in vpcs_info:\n writer.writerow({\n 'AccountName': account_name,\n 'AccountID': account_id,\n 'Region': region,\n 'VPCName': vpc['VPCName'],\n 'VPCID': vpc['VPCID'],\n 'CIDRBlock': vpc['CIDRBlock']\n })\n \n if vpcs_info:\n print(f'\"{account_name}\" (ID: {account_id}, Region: {region}) VPCs found: {len(vpcs_info)}')\n except ClientError as e:\n print(f\"Error in {region} for {account_name}: {e}\")\n except (ClientError, ProfileNotFound) as e:\n print(f\"Error accessing account {account_name}: {e}\")\n\n print(f\"\\nTotal number of VPCs found: {total_vpcs_all_accounts}\")\n print(\"CSV file 'aws_vpcs_by_region.csv' generated successfully.\")\n\nif __name__ == \"__main__\":\n main()\n" ./2026-02-12.md: "content": "import boto3\nfrom botocore.exceptions import ClientError, ProfileNotFound\nfrom aws_utils import setup_org_accounts_session\n\ndef list_rds_instances(rds_client):\n instance_names = []\n paginator = rds_client.get_paginator('describe_db_instances')\n for page in paginator.paginate():\n for db_instance in page['DBInstances']:\n instance_names.append(db_instance['DBInstanceIdentifier'])\n return instance_names\n\ndef list_rds_clusters(rds_client):\n cluster_names = []\n paginator = rds_client.get_paginator('describe_db_clusters')\n for page in paginator.paginate():\n for db_cluster in page['DBClusters']:\n cluster_names.append(db_cluster['DBClusterIdentifier'])\n return cluster_names\n\ndef main():\n ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n\n for account_id, account_name, profile_name in setup_org_accounts_session(ou_ids):\n try:\n boto3.setup_default_session(profile_name=profile_name)\n rds_client = boto3.client('rds')\n \n names = list_rds_instances(rds_client) + list_rds_clusters(rds_client)\n for name in names:\n print(f'\"{account_name}\":\"{name}\"')\n \n print(f\"Processed account '{account_name}' (ID: {account_id})\")\n except (ClientError, ProfileNotFound) as e:\n print(f\"Error accessing account '{account_name}': {e}\")\n\n print(\"\\nRDS names have been successfully listed for all accounts.\")\n\nif __name__ == \"__main__\":\n main()\n" ./2026-02-12.md: "pattern": "ou-" ./2026-02-12.md: "output": "Found 7 matches for pattern \"ou-\" in path \".\":\n---\nFile: aws-ec2-ami.py\nL54: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n---\nFile: aws-vpc.py\nL26: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n---\nFile: cloudfront.py\nL52: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n---\nFile: aws-metrics.py\nL72: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n---\nFile: aws-asg-lt-ami.py\nL134: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n---\nFile: list_rds_instances.py\nL22: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n---\nFile: aws-ips.py\nL47: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n---" ./2026-02-12.md: "content": "import boto3\nimport json\nimport os\nfrom datetime import datetime, date\nfrom dateutil.relativedelta import relativedelta\n\n# Attempt to load .env file if python-dotenv is installed\ntry:\n from dotenv import load_dotenv\n load_dotenv()\nexcept ImportError:\n pass\n\ndef get_skip_accounts():\n \"\"\"Returns a list of account IDs to skip from SKIP_ACCOUNTS env var.\"\"\"\n skip_str = os.getenv('SKIP_ACCOUNTS', '')\n if not skip_str:\n return []\n return [s.strip() for s in skip_str.split(',') if s.strip()]\n\ndef get_ou_ids():\n \"\"\"Returns a list of OU IDs from OU_IDS env var, or defaults.\"\"\"\n ou_str = os.getenv('OU_IDS', '')\n if ou_str:\n return [o.strip() for o in ou_str.split(',') if o.strip()]\n # Default OUs if none provided\n return ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n\ndef get_account_names():\n \"\"\"Fetches account names from AWS Organizations, excluding skipped accounts.\"\"\"\n org_client = boto3.client('organizations')\n skip_accounts = get_skip_accounts()\n accounts = {}\n try:\n paginator = org_client.get_paginator('list_accounts')\n for page in paginator.paginate():\n for account in page['Accounts']:\n if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts:\n accounts[account['Id']] = account['Name']\n except Exception as e:\n print(f\"Error fetching account names: {e}\")\n return accounts\n\ndef get_previous_month_range():\n \"\"\"Returns (start_date, end_date) for the previous month in YYYY-MM-DD format.\"\"\"\n today = date.today()\n first_day_curr = today.replace(day=1)\n last_day_prev = first_day_curr - relativedelta(days=1)\n start_date = last_day_prev.replace(day=1).strftime('%Y-%m-%d')\n end_date = first_day_curr.strftime('%Y-%m-%d')\n return start_date, end_date\n\ndef get_last_n_months_ranges(n=3):\n \"\"\"Returns a list of (start_date, end_date, label) for the last n months.\"\"\"\n ranges = []\n current_date = datetime.now().replace(day=1)\n for i in range(1, n + 1):\n start_dt = current_date - relativedelta(months=i)\n end_dt = current_date - relativedelta(months=i-1)\n ranges.append((\n start_dt.strftime('%Y-%m-%d'),\n end_dt.strftime('%Y-%m-%d'),\n start_dt.strftime('%Y-%m')\n ))\n return ranges\n\ndef get_aws_pricing(service_code, filters):\n \"\"\"Generic helper to fetch on-demand price from AWS Pricing API (us-east-1).\"\"\"\n pricing_client = boto3.client('pricing', region_name='us-east-1')\n try:\n response = pricing_client.get_products(\n ServiceCode=service_code,\n Filters=filters\n )\n if response['PriceList']:\n price_item = json.loads(response['PriceList'][0])\n on_demand = price_item['terms']['OnDemand']\n term_key = list(on_demand.keys())[0]\n price_dimensions = on_demand[term_key]['priceDimensions']\n dim_key = list(price_dimensions.keys())[0]\n return float(price_dimensions[dim_key]['pricePerUnit']['USD'])\n except Exception as e:\n print(f\"Error fetching pricing for {service_code}: {e}\")\n return None\n\ndef setup_org_accounts_session(ou_ids=None, profile_suffix='.admin'):\n \"\"\"\n Yields (account_id, account_name, profile_name) for active accounts in OUs.\n Handles boto3 session setup for each account.\n Excludes accounts in SKIP_ACCOUNTS env var.\n \"\"\"\n if ou_ids is None:\n ou_ids = get_ou_ids()\n \n skip_accounts = get_skip_accounts()\n org_client = boto3.client('organizations')\n for ou_id in ou_ids:\n paginator = org_client.get_paginator('list_accounts_for_parent')\n for page in paginator.paginate(ParentId=ou_id):\n for account in page['Accounts']:\n if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts:\n account_id = account['Id']\n # Sanitize account name for profile use\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}{profile_suffix}'\n yield account_id, account['Name'], profile_name\n", ./2026-02-12.md: "old_string": "def main():\n ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n regions_to_check = ['eu-west-1', 'eu-west-2']\n total_vpcs_all_accounts = 0\n\n with open('aws_vpcs_by_region.csv', mode='w', newline='') as csvfile:\n fieldnames = ['AccountName', 'AccountID', 'Region', 'VPCName', 'VPCID', 'CIDRBlock']\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n for account_id, account_name, profile_name in setup_org_accounts_session(ou_ids):\n", ./2026-02-12.md: "old_string": "def main():\n ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n\n for account_id, account_name, profile_name in setup_org_accounts_session(ou_ids):\n", ./2026-02-12.md: "output": "\nIMPORTANT: The file content has been truncated.\nStatus: Showing lines 131-237 of 237 total lines.\nAction: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: 237.\n\n--- FILE CONTENT (truncated) ---\n# NOTE: This part is taken directly from your example script's organizational structure.\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs) to check\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] \nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nprint(\"1. Fetching active accounts from specified OUs...\")\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\nprint(f\"-> Found {len(active_accounts)} active accounts to check.\")\n\nall_asg_info = []\n\n# Define the regions to check (matches your example script)\nregions_to_check = ['eu-west-1', 'eu-west-2'] \n\n# Open CSV file for writing ASG information\ncsv_filepath = 'autoscaling_group_ami_template_info.csv'\nwith open(csv_filepath, mode='w', newline='') as csvfile:\n fieldnames = [\n 'AccountName', \n 'AccountID', \n 'Region', \n 'ASG_Name', \n 'Template_Type', \n 'Template_Name', \n 'Template_Version', \n 'AMI_ID', \n 'AMI_Name'\n ]\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n print(\"\\n2. Iterating through accounts and regions to collect ASG data...\")\n for account in active_accounts:\n account_id = account['Id']\n # Sanitize account name for profile use (matching original script's logic)\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' \n \n print(f\"\\n--- Checking Account: {account_name} (ID: {account_id}) ---\")\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n # NOTE: This relies on your local AWS configuration having these profiles set up.\n boto3.setup_default_session(profile_name=profile_name)\n \n # Loop through each region\n for region in regions_to_check:\n try:\n # Initialize clients for the specific region\n asg_client = boto3.client('autoscaling', region_name=region)\n ec2_client = boto3.client('ec2', region_name=region) # Needed to resolve AMI Name and LT details\n \n # Get ASG information\n asg_data = get_asg_details(account_id, account_name, region, asg_client, ec2_client)\n \n # Write results to CSV and print to console\n for asg in asg_data:\n all_asg_info.append(asg)\n writer.writerow(asg)\n print(f\" [FOUND] ASG: {asg['ASG_Name']}, Template Type: {asg['Template_Type']}, Template Name: {asg['Template_Name']} ({asg['Template_Version']}), AMI ID: {asg['AMI_ID']}, AMI Name: {asg['AMI_Name']}\")\n\n except (ClientError) as e:\n print(f\" -> Error accessing services in region '{region}': {e}\")\n \n # Small delay to respect API limits, especially for org-wide searches\n time.sleep(0.5)\n \n except (ClientError, ProfileNotFound) as e:\n print(f\" -> Error setting up session/profile '{profile_name}': {e}\")\n\n\n# Output summary\ntotal_asgs = len(all_asg_info)\nprint(\"\\n========================================================\")\nprint(\"ASG & AMI Collection Summary:\")\nprint(f\"Total Auto Scaling Groups found: {total_asgs}\")\n\nif total_asgs > 0:\n # Basic summary of AMI usage\n ami_usage = defaultdict(int)\n for asg in all_asg_info:\n ami_usage[asg['AMI_Name']] += 1\n \n print(f\"Distinct AMI Names used: {len(ami_usage)}\")\n print(\"\\nTop 5 AMI Usage Counts:\")\n for ami_name, count in sorted(ami_usage.items(), key=lambda item: item[1], reverse=True)[:5]:\n print(f\" - {ami_name}: {count} ASGs\")\n\nprint(f\"\\nCSV file '{csv_filepath}' generated successfully.\")\nprint(\"========================================================\")\n" ./2026-02-12.md: "old_string": "# NOTE: This part is taken directly from your example script's organizational structure.\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs) to check\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] \nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nprint(\"1. Fetching active accounts from specified OUs...\")\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('Token')\n if not next_token:\n break\nprint(f\"-> Found {len(active_accounts)} active accounts to check.\")\n\nall_asg_info = []\n\n# Define the regions to check (matches your example script)\nregions_to_check = ['eu-west-1', 'eu-west-2'] \n\n# Open CSV file for writing ASG information\ncsv_filepath = 'autoscaling_group_ami_template_info.csv'\nwith open(csv_filepath, mode='w', newline='') as csvfile:\n fieldnames = [\n 'AccountName', \n 'AccountID', \n 'Region', \n 'ASG_Name', \n 'Template_Type', \n 'Template_Name', \n 'Template_Version', \n 'AMI_ID', \n 'AMI_Name'\n ]\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n print(\"\\n2. Iterating through accounts and regions to collect ASG data...\")\n for account in active_accounts:\n account_id = account['Id']\n # Sanitize account name for profile use (matching original script's logic)\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' \n \n print(f\"\\n--- Checking Account: {account_name} (ID: {account_id}) ---\")\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n # NOTE: This relies on your local AWS configuration having these profiles set up.\n boto3.setup_default_session(profile_name=profile_name)\n" ./2026-02-12.md: "old_string": "# NOTE: This part is taken directly from your example script's organizational structure.\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs) to check\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] \nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nprint(\"1. Fetching active accounts from specified OUs...\")\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\nprint(f\"-> Found {len(active_accounts)} active accounts to check.\")\n\nall_asg_info = []\n\n# Define the regions to check (matches your example script)\nregions_to_check = ['eu-west-1', 'eu-west-2'] \n\n# Open CSV file for writing ASG information\ncsv_filepath = 'autoscaling_group_ami_template_info.csv'\nwith open(csv_filepath, mode='w', newline='') as csvfile:\n fieldnames = [\n 'AccountName', \n 'AccountID', \n 'Region', \n 'ASG_Name', \n 'Template_Type', \n 'Template_Name', \n 'Template_Version', \n 'AMI_ID', \n 'AMI_Name'\n ]\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n print(\"\\n2. Iterating through accounts and regions to collect ASG data...\")\n for account in active_accounts:\n account_id = account['Id']\n # Sanitize account name for profile use (matching original script's logic)\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' \n \n print(f\"\\n--- Checking Account: {account_name} (ID: {account_id}) ---\")\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n # NOTE: This relies on your local AWS configuration having these profiles set up.\n boto3.setup_default_session(profile_name=profile_name)\n", ./2026-02-12.md: "output": "\nIMPORTANT: The file content has been truncated.\nStatus: Showing lines 51-146 of 146 total lines.\nAction: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: 146.\n\n--- FILE CONTENT (truncated) ---\n# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Initialize a dictionary to store AMI names and their counts\nami_name_counts = defaultdict(int)\nall_instances_info = []\n\n# Define the regions to check\nregions_to_check = ['eu-west-1', 'eu-west-2']\n\n# Open CSV file for writing instance information\nwith open('running_ec2_instances_info.csv', mode='w', newline='') as csvfile:\n # Add 'Region' to the fieldnames for the CSV header\n fieldnames = ['AccountName', 'AccountID', 'Region', 'InstanceName', 'InstanceID', 'AMI_ID', 'AMI_Name']\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n for account in active_accounts:\n account_id = account['Id']\n \n # Replace \" - \" with \"-\" and replace spaces with \"-\" in account names\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' # Constructing the profile name\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n boto3.setup_default_session(profile_name=profile_name)\n \n # Loop through each region\n for region in regions_to_check:\n try:\n # Initialize EC2 client for the specific region\n ec2_client = boto3.client('ec2', region_name=region)\n \n # List only running EC2 instances with name, AMI ID, and AMI name\n instances_info = list_running_ec2_instances_with_ami_info(ec2_client)\n \n # Collect instance information and count AMI names\n for instance in instances_info:\n all_instances_info.append(instance)\n ami_name_counts[instance['AMI_Name']] += 1 # Count each AMI name\n \n # Write instance information to CSV, including the region\n writer.writerow({\n 'AccountName': account_name,\n 'AccountID': account_id,\n 'Region': region,\n 'InstanceName': instance['InstanceName'],\n 'InstanceID': instance['InstanceId'],\n 'AMI_ID': instance['AMI_ID'],\n 'AMI_Name': instance['AMI_Name']\n })\n \n # Print the results for the current account and region\n print(f'\"{account_name}\" (ID: {account_id}, Region: {region}) Running EC2 Instances:')\n for instance in instances_info:\n print(f\"Instance Name: {instance['InstanceName']}, Instance ID: {instance['InstanceId']}, AMI ID: {instance['AMI_ID']}, AMI Name: {instance['AMI_Name']}\")\n print('--------------------------------------------------------')\n\n except (ClientError) as e:\n print(f\"Error accessing EC2 in region '{region}' for account '{account_name}' (ID: {account_id}): {e}\")\n \n except (ClientError, ProfileNotFound) as e:\n print(f\"Error accessing account '{account_name}' (ID: {account_id}): {e}\")\n\n# Output summary of distinct AMI names with counts and total instance details\nprint(\"\\nSummary of Running EC2 Instances and AMI Usage Across All Accounts:\")\n#output total number of runniing instances\nprint(f\"Total number of running EC2 instances across all accounts: {len(all_instances_info)}\")\nprint(f\"Total number of distinct AMI names across all accounts: {len(ami_name_counts)}\")\nprint(\"Count of each distinct AMI name across all accounts:\")\nfor ami_name, count in sorted(ami_name_counts.items()):\n print(f\"{ami_name}: {count} instances\")\n\nprint(\"\\nCSV file 'running_ec2_instances_info.csv' generated successfully.\")\n" ./2026-02-12.md: "old_string": "# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Initialize a dictionary to store AMI names and their counts\nami_name_counts = defaultdict(int)\nall_instances_info = []\n\n# Define the regions to check\nregions_to_check = ['eu-west-1', 'eu-west-2']\n\n# Open CSV file for writing instance information\nwith open('running_ec2_instances_info.csv', mode='w', newline='') as csvfile:\n # Add 'Region' to the fieldnames for the CSV header\n fieldnames = ['AccountName', 'AccountID', 'Region', 'InstanceName', 'InstanceID', 'AMI_ID', 'AMI_Name']\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n for account in active_accounts:\n account_id = account['Id']\n \n # Replace \" - \" with \"-\" and replace spaces with \"-\" in account names\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' # Constructing the profile name\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n boto3.setup_default_session(profile_name=profile_name)\n", ./2026-02-12.md: "output": "\nIMPORTANT: The file content has been truncated.\nStatus: Showing lines 1-100 of 170 total lines.\nAction: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: 100.\n\n--- FILE CONTENT (truncated) ---\nimport boto3\nimport csv\nfrom botocore.exceptions import ClientError, ProfileNotFound\n\n# Function to list public IPs, Private IPs, and Instance IDs\ndef list_ec2_instance_info(ec2_client):\n \"\"\"\n Lists the public IP, private IP, and Instance ID of EC2 instances.\n Returns a list of dictionaries.\n \"\"\"\n instance_info = []\n paginator = ec2_client.get_paginator('describe_instances')\n for page in paginator.paginate():\n for reservation in page['Reservations']:\n for instance in reservation['Instances']:\n # Only process if it has a Public IP\n if 'PublicIpAddress' in instance:\n instance_info.append({\n 'InstanceId': instance['InstanceId'],\n 'PublicIp': instance['PublicIpAddress'],\n 'PrivateIp': instance.get('PrivateIpAddress', 'N/A')\n })\n return instance_info\n\n# Function to list Elastic IPs (EIPs)\ndef list_elastic_ips(ec2_client):\n elastic_ips = []\n response = ec2_client.describe_addresses()\n for address in response['Addresses']:\n if 'PublicIp' in address:\n elastic_ips.append(address['PublicIp'])\n return elastic_ips\n\n# Function to list the DNS names of internet-facing ALBs\ndef list_alb_dns_names(elbv2_client):\n alb_dns_names = []\n response = elbv2_client.describe_load_balancers()\n for lb in response['LoadBalancers']:\n if lb['Scheme'] == 'internet-facing':\n alb_dns_names.append(lb['DNSName'])\n return alb_dns_names\n\n# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Initialize a counter for the total number of external resources\ntotal_external_resources_count = 0\n\n# Define the regions to check\nregions_to_check = ['eu-west-1', 'eu-west-2']\n\n# Open CSV file for writing\nwith open('aws_external_ips_by_region.csv', mode='w', newline='') as csvfile:\n # Updated Headers\n fieldnames = [\n 'AccountName', \n 'AccountID', \n 'Region', \n 'Type', \n 'Instance ID', \n 'Public IP/DNS Name', \n 'Private IP', \n 'Private Hostname'\n ]\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n for account in active_accounts:\n account_id = account['Id']\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin'\n \n try:\n boto3.setup_default_session(profile_name=profile_name)\n \n for region in regions_to_check:\n try:\n ec2_client = boto3.client('ec2', region_name=region)\n elbv2_client = boto3.client('elbv2', region_name=region)\n " ./2026-02-12.md: "old_string": "# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Initialize a counter for the total number of external resources\ntotal_external_resources_count = 0\n\n# Define the regions to check\nregions_to_check = ['eu-west-1', 'eu-west-2']\n\n# Open CSV file for writing\nwith open('aws_external_ips_by_region.csv', mode='w', newline='') as csvfile:\n # Updated Headers\n fieldnames = [\n 'AccountName', \n 'AccountID', \n 'Region', \n 'Type', \n 'Instance ID', \n 'Public IP/DNS Name', \n 'Private IP', \n 'Private Hostname'\n ]\n writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n writer.writeheader()\n\n # Iterate through active accounts and then through the regions\n for account in active_accounts:\n account_id = account['Id']\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin'\n \n try:\n boto3.setup_default_session(profile_name=profile_name)\n", ./2026-02-12.md: "output": "\nIMPORTANT: The file content has been truncated.\nStatus: Showing lines 1-100 of 168 total lines.\nAction: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: 100.\n\n--- FILE CONTENT (truncated) ---\nimport boto3\nimport csv\nfrom datetime import datetime, timedelta, timezone\nfrom botocore.exceptions import ClientError, ProfileNotFound\n\n# Function to get CloudFront log size for the last month\ndef get_cloudfront_log_size(cloudfront_client, s3_client, distribution_id):\n try:\n # Get the distribution configuration\n distribution_config_response = cloudfront_client.get_distribution_config(Id=distribution_id)\n distribution_config = distribution_config_response['DistributionConfig']\n\n # Check if logging is enabled\n logging_config = distribution_config.get('Logging', {})\n if not logging_config.get('Enabled', False):\n return 0\n\n # Get the S3 bucket and prefix for logs\n s3_bucket_name = logging_config['Bucket'].split('.')[0]\n s3_bucket_prefix = logging_config.get('Prefix', '')\n\n # Set time range for the last month\n end_date = datetime.now(timezone.utc) # Current UTC time, timezone-aware\n start_date = end_date - timedelta(days=30)\n\n total_size = 0\n paginator = s3_client.get_paginator('list_objects_v2')\n for page in paginator.paginate(Bucket=s3_bucket_name, Prefix=s3_bucket_prefix):\n for obj in page.get('Contents', []):\n # Make sure 'LastModified' is compared as a timezone-aware datetime\n if start_date <= obj['LastModified'].replace(tzinfo=timezone.utc) <= end_date:\n total_size += obj['Size']\n\n return total_size\n except KeyError as e:\n print(f\"Error retrieving CloudFront logs for distribution {distribution_id}: Missing key {e}\")\n return 0\n except ClientError as e:\n print(f\"Error accessing CloudFront or S3 for distribution {distribution_id}: {e}\")\n return 0\n\n# Function to count EC2 instances\ndef count_ec2_instances(ec2_client):\n instances = ec2_client.describe_instances()\n instance_count = sum([len(reservation['Instances']) for reservation in instances['Reservations']])\n return instance_count\n\n# Function to count running ECS containers\ndef count_ecs_containers(ecs_client):\n cluster_arns = ecs_client.list_clusters()['clusterArns']\n running_tasks = 0\n for cluster_arn in cluster_arns:\n response = ecs_client.describe_clusters(clusters=[cluster_arn])\n if response['clusters']:\n running_tasks += response['clusters'][0]['runningTasksCount']\n return running_tasks\n\n# Function to count RDS instances\ndef count_rds_instances(rds_client):\n instances = rds_client.describe_db_instances()\n return len(instances['DBInstances'])\n\n# Function to count RDS clusters\ndef count_rds_clusters(rds_client):\n clusters = rds_client.describe_db_clusters()\n return len(clusters['DBClusters'])\n\n# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Initialize total counters\ntotal_accounts = 0\ntotal_ec2_instances = 0\ntotal_ecs_containers = 0\ntotal_rds_instances = 0\ntotal_rds_clusters = 0\ntotal_cloudfront_log_size_bytes = 0\n\n# Prepare CSV file\nwith open('aws_account_metrics.csv', mode='w', newline='') as file:\n writer = csv.writer(file)" ./2026-02-12.md: "old_string": "# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Initialize total counters\ntotal_accounts = 0\ntotal_ec2_instances = 0\ntotal_ecs_containers = 0\ntotal_rds_instances = 0\ntotal_rds_clusters = 0\ntotal_cloudfront_log_size_bytes = 0\n\n# Prepare CSV file\nwith open('aws_account_metrics.csv', mode='w', newline='') as file:\n writer = csv.writer(file)\n writer.writerow(['AccountName', 'AccountID', 'EC2Instances', 'ECSContainers', 'RDSInstances', 'RDSClusters', 'CloudFrontLogSizeGB'])\n\n # Iterate through each active account\n for account in active_accounts:\n account_id = account['Id']\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin'\n \n try:\n # Set up session using the constructed profile name\n boto3.setup_default_session(profile_name=profile_name)\n", ./2026-02-12.md: "output": "import boto3\nimport json\nimport os\nfrom datetime import datetime, date\nfrom dateutil.relativedelta import relativedelta\n\n# Attempt to load .env file if python-dotenv is installed\ntry:\n from dotenv import load_dotenv\n load_dotenv()\nexcept ImportError:\n pass\n\ndef get_skip_accounts():\n \"\"\"Returns a list of account IDs to skip from SKIP_ACCOUNTS env var.\"\"\"\n skip_str = os.getenv('SKIP_ACCOUNTS', '')\n if not skip_str:\n return []\n return [s.strip() for s in skip_str.split(',') if s.strip()]\n\ndef get_ou_ids():\n \"\"\"Returns a list of OU IDs from OU_IDS env var, or defaults.\"\"\"\n ou_str = os.getenv('OU_IDS', '')\n if ou_str:\n return [o.strip() for o in ou_str.split(',') if o.strip()]\n # Default OUs if none provided\n return ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n\ndef get_account_names():\n \"\"\"Fetches account names from AWS Organizations, excluding skipped accounts.\"\"\"\n org_client = boto3.client('organizations')\n skip_accounts = get_skip_accounts()\n accounts = {}\n try:\n paginator = org_client.get_paginator('list_accounts')\n for page in paginator.paginate():\n for account in page['Accounts']:\n if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts:\n accounts[account['Id']] = account['Name']\n except Exception as e:\n print(f\"Error fetching account names: {e}\")\n return accounts\n\ndef get_previous_month_range():\n \"\"\"Returns (start_date, end_date) for the previous month in YYYY-MM-DD format.\"\"\"\n today = date.today()\n first_day_curr = today.replace(day=1)\n last_day_prev = first_day_curr - relativedelta(days=1)\n start_date = last_day_prev.replace(day=1).strftime('%Y-%m-%d')\n end_date = first_day_curr.strftime('%Y-%m-%d')\n return start_date, end_date\n\ndef get_last_n_months_ranges(n=3):\n \"\"\"Returns a list of (start_date, end_date, label) for the last n months.\"\"\"\n ranges = []\n current_date = datetime.now().replace(day=1)\n for i in range(1, n + 1):\n start_dt = current_date - relativedelta(months=i)\n end_dt = current_date - relativedelta(months=i-1)\n ranges.append((\n start_dt.strftime('%Y-%m-%d'),\n end_dt.strftime('%Y-%m-%d'),\n start_dt.strftime('%Y-%m')\n ))\n return ranges\n\ndef get_aws_pricing(service_code, filters):\n \"\"\"Generic helper to fetch on-demand price from AWS Pricing API (us-east-1).\"\"\"\n pricing_client = boto3.client('pricing', region_name='us-east-1')\n try:\n response = pricing_client.get_products(\n ServiceCode=service_code,\n Filters=filters\n )\n if response['PriceList']:\n price_item = json.loads(response['PriceList'][0])\n on_demand = price_item['terms']['OnDemand']\n term_key = list(on_demand.keys())[0]\n price_dimensions = on_demand[term_key]['priceDimensions']\n dim_key = list(price_dimensions.keys())[0]\n return float(price_dimensions[dim_key]['pricePerUnit']['USD'])\n except Exception as e:\n print(f\"Error fetching pricing for {service_code}: {e}\")\n return None\n\ndef setup_org_accounts_session(ou_ids=None, profile_suffix='.admin'):\n \"\"\"\n Yields (account_id, account_name, profile_name) for active accounts in OUs.\n Handles boto3 session setup for each account.\n Excludes accounts in SKIP_ACCOUNTS env var.\n \"\"\"\n if ou_ids is None:\n ou_ids = get_ou_ids()\n \n skip_accounts = get_skip_accounts()\n org_client = boto3.client('organizations')\n for ou_id in ou_ids:\n paginator = org_client.get_paginator('list_accounts_for_parent')\n for page in paginator.paginate(ParentId=ou_id):\n for account in page['Accounts']:\n if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts:\n account_id = account['Id']\n # Sanitize account name for profile use\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}{profile_suffix}'\n yield account_id, account['Name'], profile_name\n" ./2026-02-12.md: "old_string": "# Initialize AWS Organizations client\norg_client = boto3.client('organizations')\n\n# List of specific Organizational Units (OUs)\nou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nactive_accounts = []\n\n# Get all active accounts for the specified OUs, handling pagination\nfor ou_id in ou_ids:\n next_token = None\n while True:\n if next_token:\n response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token)\n else:\n response = org_client.list_accounts_for_parent(ParentId=ou_id)\n \n active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE'])\n \n next_token = response.get('NextToken')\n if not next_token:\n break\n\n# Initialize total counters\ntotal_accounts = 0\ntotal_ec2_instances = 0\ntotal_ecs_containers = 0\ntotal_rds_instances = 0\ntotal_rds_clusters = 0\ntotal_cloudfront_log_size_bytes = 0\n\n# Prepare CSV file\nwith open('aws_account_metrics.csv', mode='w', newline='') as file:\n writer = csv.writer(file)\n writer.writerow(['Account ID', 'Account Email', 'EC2 Instance Count', 'ECS Running Containers', 'RDS Instances', 'RDS Clusters', 'CloudFront Log Size (GB)'])\n\n # Iterate through active accounts\n for account in active_accounts:\n account_id = account['Id']\n \n # Replace \" - \" with \"-\" and replace spaces with \"-\" in account names\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}.admin' # Constructing the profile name\n account_email = account['Email']\n \n try:\n # Set up the default session using the AWS SSO profile for each account\n boto3.setup_default_session(profile_name=profile_name)\n", ./2026-02-12.md: "old_string": "def get_ou_ids():\n \"\"\"Returns a list of OU IDs from OU_IDS env var, or defaults.\"\"\"\n ou_str = os.getenv('OU_IDS', '')\n if ou_str:\n return [o.strip() for o in o.strip().split(',') if o.strip()]\n # Default OUs if none provided\n return ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n", ./2026-02-12.md: "output": "import boto3\nimport json\nimport os\nfrom datetime import datetime, date\nfrom dateutil.relativedelta import relativedelta\n\n# Attempt to load .env file if python-dotenv is installed\ntry:\n from dotenv import load_dotenv\n load_dotenv()\nexcept ImportError:\n pass\n\ndef get_skip_accounts():\n \"\"\"Returns a list of account IDs to skip from SKIP_ACCOUNTS env var.\"\"\"\n skip_str = os.getenv('SKIP_ACCOUNTS', '')\n if not skip_str:\n return []\n return [s.strip() for s in skip_str.split(',') if s.strip()]\n\ndef get_ou_ids():\n \"\"\"Returns a list of OU IDs from OU_IDS env var, or defaults.\"\"\"\n ou_str = os.getenv('OU_IDS', '')\n if ou_str:\n return [o.strip() for o in ou_str.split(',') if o.strip()]\n # Default OUs if none provided\n return ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n\ndef get_account_names():\n \"\"\"Fetches account names from AWS Organizations, excluding skipped accounts.\"\"\"\n org_client = boto3.client('organizations')\n skip_accounts = get_skip_accounts()\n accounts = {}\n try:\n paginator = org_client.get_paginator('list_accounts')\n for page in paginator.paginate():\n for account in page['Accounts']:\n if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts:\n accounts[account['Id']] = account['Name']\n except Exception as e:\n print(f\"Error fetching account names: {e}\")\n return accounts\n\ndef get_previous_month_range():\n \"\"\"Returns (start_date, end_date) for the previous month in YYYY-MM-DD format.\"\"\"\n today = date.today()\n first_day_curr = today.replace(day=1)\n last_day_prev = first_day_curr - relativedelta(days=1)\n start_date = last_day_prev.replace(day=1).strftime('%Y-%m-%d')\n end_date = first_day_curr.strftime('%Y-%m-%d')\n return start_date, end_date\n\ndef get_last_n_months_ranges(n=3):\n \"\"\"Returns a list of (start_date, end_date, label) for the last n months.\"\"\"\n ranges = []\n current_date = datetime.now().replace(day=1)\n for i in range(1, n + 1):\n start_dt = current_date - relativedelta(months=i)\n end_dt = current_date - relativedelta(months=i-1)\n ranges.append((\n start_dt.strftime('%Y-%m-%d'),\n end_dt.strftime('%Y-%m-%d'),\n start_dt.strftime('%Y-%m')\n ))\n return ranges\n\ndef get_aws_pricing(service_code, filters):\n \"\"\"Generic helper to fetch on-demand price from AWS Pricing API (us-east-1).\"\"\"\n pricing_client = boto3.client('pricing', region_name='us-east-1')\n try:\n response = pricing_client.get_products(\n ServiceCode=service_code,\n Filters=filters\n )\n if response['PriceList']:\n price_item = json.loads(response['PriceList'][0])\n on_demand = price_item['terms']['OnDemand']\n term_key = list(on_demand.keys())[0]\n price_dimensions = on_demand[term_key]['priceDimensions']\n dim_key = list(price_dimensions.keys())[0]\n return float(price_dimensions[dim_key]['pricePerUnit']['USD'])\n except Exception as e:\n print(f\"Error fetching pricing for {service_code}: {e}\")\n return None\n\ndef setup_org_accounts_session(ou_ids=None, profile_suffix='.admin'):\n \"\"\"\n Yields (account_dict, profile_name) for active accounts in OUs.\n Handles boto3 session setup for each account.\n Excludes accounts in SKIP_ACCOUNTS env var.\n \"\"\"\n if ou_ids is None:\n ou_ids = get_ou_ids()\n \n skip_accounts = get_skip_accounts()\n org_client = boto3.client('organizations')\n for ou_id in ou_ids:\n paginator = org_client.get_paginator('list_accounts_for_parent')\n for page in paginator.paginate(ParentId=ou_id):\n for account in page['Accounts']:\n if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts:\n # Sanitize account name for profile use\n account_name = account['Name'].replace(\" - \", \"-\").replace(\" \", \"-\")\n profile_name = f'{account_name}{profile_suffix}'\n yield account, profile_name\n" ./2026-02-12.md: "old_string": "def get_ou_ids():\n \"\"\"Returns a list of OU IDs from OU_IDS env var, or defaults.\"\"\"\n ou_str = os.getenv('OU_IDS', '')\n if ou_str:\n return [o.strip() for o in ou_str.split(',') if o.strip()]\n # Default OUs if none provided\n return ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\n", ./2026-02-12.md: "content": "# AWS Organization Unit IDs to scan (comma-separated)\n# OU_IDS=ou-h0vo-0waugyoz,ou-h0vo-32wgvd6w\n\n# AWS Account IDs to skip (comma-separated)\n# SKIP_ACCOUNTS=123456789012,987654321098\n", ./2026-02-12.md:I will sanitize the codebase by replacing specific identifiers like `dxw.admin` and `ou-h0vo-...` with generic placeholders in `aws_utils.py`, `setup.sh`, and `.env.example`. ./2026-02-12.md: "content": "# Parent AWS profile for Organizations and SSO login\n# AWS_PARENT_PROFILE=my-parent-profile\n\n# AWS Organization Unit IDs to scan (comma-separated)\n# OU_IDS=ou-1234-abcd5678,ou-9012-efgh3456\n\n# AWS Account IDs to skip (comma-separated)\n# SKIP_ACCOUNTS=123456789012,987654321098\n", ./2026-02-12.md: "pattern": "ou-h0vo|dxw\\.admin" ./2026-02-12.md: "output": "Found 3 matches for pattern \"ou-h0vo|dxw\\.admin\" in path \".\":\n---\nFile: cloudfront.py\nL52: ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w']\nL74: # Set up the default session for the billing account ('dxw.admin') to access cost data\nL75: boto3.setup_default_session(profile_name='dxw.admin')\n---" ./2026-02-12.md: "pattern": "ou-h0vo|dxw\\.admin" ./2026-02-12.md: "output": "No matches found for pattern \"ou-h0vo|dxw\\.admin\" in path \".\"." ./2026-02-12.md:I'll check for any remaining hardcoded OU strings by searching for the "ou-" prefix.**Tool Command**: ./2026-02-12.md: "pattern": "ou-([a-z0-9]+)-([a-z0-9]+)" ./2026-02-12.md: "output": "No matches found for pattern \"ou-([a-z0-9]+)-([a-z0-9]+)\" in path \".\"." ./2026-02-12.md: "output": "Output: # Parent AWS profile for Organizations and SSO login\nAWS_PARENT_PROFILE=\"dxw.admin\"\n\n# AWS Organization Unit IDs to scan (comma-separated)\n# OU_IDS=ou-1234-abcd5678,ou-9012-efgh3456\n\n# AWS Account IDs to skip (comma-separated)\n# SKIP_ACCOUNTS=123456789012,987654321098\nProcess Group PGID: 38238" ./2026-02-12.md: "content": "# Gemini CLI Project Context - AWS Organization Audit Scripts\n\n## Project Overview\nThis repository contains a collection of Python scripts for auditing AWS Organizations. It handles multi-account discovery, SSO authentication, and generates reports on costs, bandwidth, and resource inventory.\n\n## Core Guidelines\n\n- **Shared Logic:** All AWS interaction MUST go through `aws_utils.py`. Never initialize `boto3.client` directly without first ensuring a session via `aws_utils`.\n- **Authentication:** Use `ensure_sso_login()` to handle AWS SSO sessions. This triggers `aws sso login` via the CLI if the session is expired.\n- **Session Management:** \n - Use `get_boto_session()` for parent/management account tasks (Organizations, Pricing API).\n - Use `setup_org_accounts_session()` to iterate through accounts in specific OUs or the entire organization. This handles profile switching for multi-account scans.\n- **Configuration:** \n - Honor environment variables: `AWS_PARENT_PROFILE`, `OU_IDS`, and `SKIP_ACCOUNTS`.\n - Support `.env` files via `python-dotenv` (handled in `aws_utils.py`).\n- **Reporting:** \n - For large datasets, prefer CSV output using the `csv` module.\n - For console summaries, use formatted tables with `print` or `pandas`.\n\n## Key Patterns\n\n### Account Iteration\nAlways use the generator pattern for cross-account audits:\n```python\nfrom aws_utils import setup_org_accounts_session\nfor account, profile_name in setup_org_accounts_session():\n boto3.setup_default_session(profile_name=profile_name)\n # ... account-specific logic ...\n```\n\n### Date Handling\nStandardize billing periods using `aws_utils.py` helpers:\n- `get_previous_month_range()`: Returns `(start, end)` for the last full month.\n- `get_last_n_months_ranges(n)`: Returns list of ranges for historical analysis.\n\n## Tooling & Dependencies\n- **Primary:** `boto3`, `pandas`, `python-dotenv`.\n- **Secondary:** `gspread` (for Google Sheets reports).\n- **Environment:** Always assume execution within a virtual environment (`.venv`).\n\n## Security & Privacy\n- **Identifiers:** Never hardcode Account IDs, OU IDs, or Profile Names. Use the configuration patterns in `aws_utils`.\n- **Sanitization:** When creating examples or documentation, use generic placeholders like `ou-1234-abcd5678` or `123456789012`.\n" ./2026-02-12.md: "content": "import unittest\nfrom unittest.mock import patch, MagicMock\nimport os\nimport sys\nfrom datetime import date\n\n# Add the project root to sys.path so we can import aws_utils\nsys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))\n\nimport aws_utils\n\nclass TestAwsUtils(unittest.TestCase):\n\n def test_get_previous_month_range(self):\n start, end = aws_utils.get_previous_month_range()\n self.assertTrue(len(start) == 10) # YYYY-MM-DD\n self.assertTrue(len(end) == 10)\n self.assertTrue(start.endswith(\"-01\"))\n self.assertTrue(end.endswith(\"-01\"))\n\n def test_get_last_n_months_ranges(self):\n ranges = aws_utils.get_last_n_months_ranges(3)\n self.assertEqual(len(ranges), 3)\n for start, end, label in ranges:\n self.assertEqual(len(start), 10)\n self.assertEqual(len(end), 10)\n self.assertEqual(len(label), 7) # YYYY-MM\n\n @patch.dict(os.environ, {'SKIP_ACCOUNTS': '123, 456 '})\n def test_get_skip_accounts(self):\n skipped = aws_utils.get_skip_accounts()\n self.assertEqual(skipped, ['123', '456'])\n\n @patch.dict(os.environ, {'SKIP_ACCOUNTS': ''})\n def test_get_skip_accounts_empty(self):\n skipped = aws_utils.get_skip_accounts()\n self.assertEqual(skipped, [])\n\n @patch.dict(os.environ, {'OU_IDS': 'ou-1, ou-2'})\n def test_get_ou_ids(self):\n ous = aws_utils.get_ou_ids()\n self.assertEqual(ous, ['ou-1', 'ou-2'])\n\n @patch.dict(os.environ, {'OU_IDS': ''})\n def test_get_ou_ids_empty(self):\n ous = aws_utils.get_ou_ids()\n self.assertEqual(ous, [])\n\n @patch('aws_utils.get_boto_session')\n def test_get_account_names(self, mock_get_session):\n # Mock session and client\n mock_session = MagicMock()\n mock_client = MagicMock()\n mock_get_session.return_value = mock_session\n mock_session.client.return_value = mock_client\n\n # Mock pagination\n mock_paginator = MagicMock()\n mock_client.get_paginator.return_value = mock_paginator\n mock_paginator.paginate.return_value = [\n {\n 'Accounts': [\n {'Id': '111', 'Name': 'Acc1', 'Status': 'ACTIVE'},\n {'Id': '222', 'Name': 'Acc2', 'Status': 'SUSPENDED'},\n {'Id': '333', 'Name': 'Acc3', 'Status': 'ACTIVE'}\n ]\n }\n ]\n\n # Mock skip accounts to be empty for this test\n with patch('aws_utils.get_skip_accounts', return_value=[]):\n accounts = aws_utils.get_account_names()\n \n self.assertEqual(len(accounts), 2)\n self.assertEqual(accounts['111'], 'Acc1')\n self.assertEqual(accounts['333'], 'Acc3')\n # Ensure SUSPENDED account is ignored\n self.assertNotIn('222', accounts)\n\n @patch('aws_utils.get_boto_session')\n def test_get_account_names_with_skip(self, mock_get_session):\n mock_session = MagicMock()\n mock_client = MagicMock()\n mock_get_session.return_value = mock_session\n mock_session.client.return_value = mock_client\n\n mock_paginator = MagicMock()\n mock_client.get_paginator.return_value = mock_paginator\n mock_paginator.paginate.return_value = [\n {\n 'Accounts': [\n {'Id': '111', 'Name': 'Acc1', 'Status': 'ACTIVE'},\n {'Id': '222', 'Name': 'Acc2', 'Status': 'ACTIVE'}\n ]\n }\n ]\n\n with patch('aws_utils.get_skip_accounts', return_value=['222']):\n accounts = aws_utils.get_account_names()\n \n self.assertEqual(len(accounts), 1)\n self.assertEqual(accounts['111'], 'Acc1')\n self.assertNotIn('222', accounts)\n\n @patch('aws_utils.get_boto_session')\n def test_setup_org_accounts_session_explicit_ou(self, mock_get_session):\n mock_session = MagicMock()\n mock_org_client = MagicMock()\n mock_get_session.return_value = mock_session\n mock_session.client.return_value = mock_org_client\n\n mock_paginator = MagicMock()\n mock_org_client.get_paginator.return_value = mock_paginator\n mock_paginator.paginate.return_value = [\n {\n 'Accounts': [\n {'Id': '111', 'Name': 'Acc 1', 'Status': 'ACTIVE'}\n ]\n }\n ]\n\n with patch('aws_utils.get_skip_accounts', return_value=[]):\n # Iterate through generator\n results = list(aws_utils.setup_org_accounts_session(ou_ids=['ou-test']))\n \n self.assertEqual(len(results), 1)\n account, profile = results[0]\n self.assertEqual(account['Id'], '111')\n self.assertEqual(profile, 'Acc-1.admin') # Checks sanitization of name\n\n @patch('aws_utils.get_boto_session')\n def test_setup_org_accounts_session_fallback(self, mock_get_session):\n mock_session = MagicMock()\n mock_org_client = MagicMock()\n mock_get_session.return_value = mock_session\n mock_session.client.return_value = mock_org_client\n\n mock_paginator = MagicMock()\n mock_org_client.get_paginator.return_value = mock_paginator\n mock_paginator.paginate.return_value = [\n {\n 'Accounts': [\n {'Id': '999', 'Name': 'FallbackAcc', 'Status': 'ACTIVE'}\n ]\n }\n ]\n\n with patch('aws_utils.get_ou_ids', return_value=[]), \\\n patch('aws_utils.get_skip_accounts', return_value=[]):\n \n results = list(aws_utils.setup_org_accounts_session())\n \n self.assertEqual(len(results), 1)\n account, profile = results[0]\n self.assertEqual(account['Id'], '999')\n # Check that it called list_accounts (fallback) not list_accounts_for_parent\n mock_org_client.get_paginator.assert_called_with('list_accounts')\n\nif __name__ == '__main__':\n unittest.main()\n", ./2026-02-12.md: "output": "# Gemini CLI Project Context - AWS Organization Audit Scripts\n\n## Project Overview\nThis repository contains a collection of Python scripts for auditing AWS Organizations. It handles multi-account discovery, SSO authentication, and generates reports on costs, bandwidth, and resource inventory.\n\n## Core Guidelines\n\n- **Shared Logic:** All AWS interaction MUST go through `aws_utils.py`. Never initialize `boto3.client` directly without first ensuring a session via `aws_utils`.\n- **Authentication:** Use `ensure_sso_login()` to handle AWS SSO sessions. This triggers `aws sso login` via the CLI if the session is expired.\n- **Session Management:** \n - Use `get_boto_session()` for parent/management account tasks (Organizations, Pricing API).\n - Use `setup_org_accounts_session()` to iterate through accounts in specific OUs or the entire organization. This handles profile switching for multi-account scans.\n- **Configuration:** \n - Honor environment variables: `AWS_PARENT_PROFILE`, `OU_IDS`, and `SKIP_ACCOUNTS`.\n - Support `.env` files via `python-dotenv` (handled in `aws_utils.py`).\n- **Reporting:** \n - For large datasets, prefer CSV output using the `csv` module.\n - For console summaries, use formatted tables with `print` or `pandas`.\n\n## Key Patterns\n\n### Account Iteration\nAlways use the generator pattern for cross-account audits:\n```python\nfrom aws_utils import setup_org_accounts_session\nfor account, profile_name in setup_org_accounts_session():\n boto3.setup_default_session(profile_name=profile_name)\n # ... account-specific logic ...\n```\n\n### Date Handling\nStandardize billing periods using `aws_utils.py` helpers:\n- `get_previous_month_range()`: Returns `(start, end)` for the last full month.\n- `get_last_n_months_ranges(n)`: Returns list of ranges for historical analysis.\n\n## Tooling & Dependencies\n- **Primary:** `boto3`, `pandas`, `python-dotenv`.\n- **Secondary:** `gspread` (for Google Sheets reports).\n- **Environment:** Always assume execution within a virtual environment (`.venv`).\n\n## Security & Privacy\n- **Identifiers:** Never hardcode Account IDs, OU IDs, or Profile Names. Use the configuration patterns in `aws_utils`.\n- **Sanitization:** When creating examples or documentation, use generic placeholders like `ou-1234-abcd5678` or `123456789012`.\n" ./2026-02-12.md: "content": "# Gemini CLI Project Context - AWS Organization Audit Scripts\n\n## Project Overview\nThis repository contains a collection of Python scripts for auditing AWS Organizations. It handles multi-account discovery, SSO authentication, and generates reports on costs, bandwidth, and resource inventory.\n\n## Core Guidelines\n\n- **Shared Logic:** All AWS interaction MUST go through `aws_utils.py`. Never initialize `boto3.client` directly without first ensuring a session via `aws_utils`.\n- **Authentication:** Use `ensure_sso_login()` to handle AWS SSO sessions. This triggers `aws sso login` via the CLI if the session is expired.\n- **Session Management:** \n - Use `get_boto_session()` for parent/management account tasks (Organizations, Pricing API).\n - Use `setup_org_accounts_session()` to iterate through accounts in specific OUs or the entire organization. This handles profile switching for multi-account scans and unsets conflicting IAM env vars.\n- **Configuration:** \n - Honor environment variables: `AWS_PARENT_PROFILE`, `OU_IDS`, `SKIP_ACCOUNTS`, `GOOGLE_CLIENT_SECRET_FILE`, and `GOOGLE_AUTHORIZED_USER_FILE`.\n - Support `.env` files via `python-dotenv` (handled in `aws_utils.py`).\n- **Reporting:** \n - For large datasets, prefer CSV output using the `csv` module.\n - **Google Sheets:** Use `export_to_sheets(title, headers, rows)` from `aws_utils.py` to automatically export CSV data to Google Sheets.\n - For console summaries, use formatted tables with `print` or `pandas`.\n\n## Key Patterns\n\n### Account Iteration\nAlways use the generator pattern for cross-account audits:\n```python\nfrom aws_utils import setup_org_accounts_session\nfor account, profile_name in setup_org_accounts_session():\n boto3.setup_default_session(profile_name=profile_name)\n # ... account-specific logic ...\n```\n\n### Date Handling\nStandardize billing periods using `aws_utils.py` helpers:\n- `get_previous_month_range()`: Returns `(start, end)` for the last full month.\n- `get_last_n_months_ranges(n)`: Returns list of ranges for historical analysis.\n\n### Testing\n- Place tests in the `tests/` directory.\n- Use `unittest` and `unittest.mock`.\n- **Mocks:** Always mock `aws_utils.get_boto_session` and AWS clients to ensure tests run without credentials or network access.\n\n## Tooling & Dependencies\n- **Primary:** `boto3`, `pandas`, `python-dotenv`.\n- **Secondary:** `gspread`, `google-auth-oauthlib` (for Google Sheets reports).\n- **Environment:** Always execute within the project's virtual environment (`.venv`).\n\n## Security & Privacy\n- **Identifiers:** Never hardcode Account IDs, OU IDs, or Profile Names. Use the configuration patterns in `aws_utils`.\n- **Sanitization:** When creating examples or documentation, use generic placeholders like `ou-1234-abcd5678` or `123456789012`.\n- **Environment:** Do not commit `.env` files or Google authentication JSON files (`client_secret.json`, `authorized_user.json`).\n", Binary file ./tests/__pycache__/test_aws_utils.cpython-312.pyc matches ./tests/test_aws_utils.py: @patch.dict(os.environ, {'OU_IDS': 'ou-1, ou-2'}) ./tests/test_aws_utils.py: self.assertEqual(ous, ['ou-1', 'ou-2']) ./tests/test_aws_utils.py: results = list(aws_utils.setup_org_accounts_session(ou_ids=['ou-test'])) ./.env:OU_IDS=ou-h0vo-32wgvd6w,ou-h0vo-0waugyoz ./.venv/lib/python3.12/site-packages/pyparsing/core.py: # ref: https://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression Binary file ./.venv/lib/python3.12/site-packages/pyparsing/__pycache__/core.cpython-312.pyc matches ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "TargetId": "ou-examplerootid111-exampleouid111" ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Arn": "arn:aws:organizations::111111111111:ou/o-exampleorgid/ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "OrganizationalUnitId": "ou-examplerootid111-exampleouid111" ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "OrganizationalUnitId": "ou-examplerootid111-exampleouid111" ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Arn": "arn:aws:organizations::111111111111:ou/o-exampleorgid/ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "TargetId": "ou-examplerootid111-exampleouid111" ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "id": "to-detach-a-policy-from-a-root-ou-or-account", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "ParentId": "ou-examplerootid111-exampleouid111" ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "id": "to-retrieve-a-list-of-all-of-the-accounts-in-a-root-or-ou-1472509590974", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "ParentId": "ou-examplerootid111-exampleouid111" ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid222", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Arn": "arn:aws:organizations::111111111111:ou/o-exampleorgid/ou-examlerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Arn": "arn:aws:organizations::111111111111:ou/o-exampleorgid/ou-examlerootid111-exampleouid222", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid222", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "id": "to-retrieve-a-list-of-all-of-the-parents-of-a-child-ou-or-account", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "id": "to-retrieve-a-list-of-policies-attached-to-a-root-ou-or-account", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Arn": "arn:aws:organizations::111111111111:ou/o-exampleorgid/ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "TargetId": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "DestinationParentId": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "id": "to-move-an-ou-or-account-to-another-ou-or-the-root", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "OrganizationalUnitId": "ou-examplerootid111-exampleouid111" ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Arn": "arn:aws:organizations::111111111111:ou/o-exampleorgid/ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/organizations/2016-11-28/examples-1.json: "Id": "ou-examplerootid111-exampleouid111", ./.venv/lib/python3.12/site-packages/botocore/data/glacier/2012-06-01/examples-1.json: "id": "to-get-information-about-a-job-you-previously-initiated-1481840928592", ./.venv/lib/python3.12/site-packages/botocore/data/iam/2010-05-08/examples-1.json: "EntityPath": "o-a1b2c3d4e5/r-f6g7h8i9j0example/ou-1a2b3c-k9l8m7n6o5example" ./.venv/lib/python3.12/site-packages/botocore/data/iam/2010-05-08/examples-1.json: "description": "The following operation generates a report for the organizational unit ou-rge0-awexample", ./.venv/lib/python3.12/site-packages/botocore/data/iam/2010-05-08/examples-1.json: "EntityPath": "o-a1b2c3d4e5/r-f6g7h8i9j0example/ou-1a2b3c-k9l8m7n6o5example/111122223333", ./.venv/lib/python3.12/site-packages/botocore/data/iam/2010-05-08/examples-1.json: "EntityPath": "o-a1b2c3d4e5/r-f6g7h8i9j0example/ou-1a2b3c-k9l8m7n6o5example/123456789012", ./.venv/lib/python3.12/site-packages/botocore/data/snowball/2016-06-30/examples-1.json: "id": "to-see-your-snowball-service-limit-and-the-number-of-snowballs-you-have-in-use-1482863394588", ./.venv/lib/python3.12/site-packages/pandas/core/strings/accessor.py: 0 caribou--- ./.venv/lib/python3.12/site-packages/pandas/core/strings/accessor.py: 0 -caribou-- Binary file ./.venv/lib/python3.12/site-packages/pandas/core/strings/__pycache__/accessor.cpython-312.pyc matches ./.venv/lib/python3.12/site-packages/pandas/io/sql.py: # See https://stackoverflow.com/questions/6514274/how-do-you-escape-strings\ ./GEMINI.md:- **Sanitization:** When creating examples or documentation, use generic placeholders like `ou-1234-abcd5678` or `123456789012`. ./.env.example:# OU_IDS=ou-1234-abcd5678,ou-9012-efgh3456 Binary file ./.git/objects/8e/30703583b0929b35edb22357a78a7962583823 matches commit ec16e288a4c1519ff236d7f3cec6f0a8b04f08a8 Author: bob walker Date: Thu Feb 12 18:13:01 2026 +0000 add test suite for aws_utils and script smoke tests diff --git a/tests/test_aws_utils.py b/tests/test_aws_utils.py new file mode 100644 index 0000000..38220ef --- /dev/null +++ b/tests/test_aws_utils.py @@ -0,0 +1,160 @@ +import unittest +from unittest.mock import patch, MagicMock +import os +import sys +from datetime import date + +# Add the project root to sys.path so we can import aws_utils +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import aws_utils + +class TestAwsUtils(unittest.TestCase): + + def test_get_previous_month_range(self): + start, end = aws_utils.get_previous_month_range() + self.assertTrue(len(start) == 10) # YYYY-MM-DD + self.assertTrue(len(end) == 10) + self.assertTrue(start.endswith("-01")) + self.assertTrue(end.endswith("-01")) + + def test_get_last_n_months_ranges(self): + ranges = aws_utils.get_last_n_months_ranges(3) + self.assertEqual(len(ranges), 3) + for start, end, label in ranges: + self.assertEqual(len(start), 10) + self.assertEqual(len(end), 10) + self.assertEqual(len(label), 7) # YYYY-MM + + @patch.dict(os.environ, {'SKIP_ACCOUNTS': '123, 456 '}) + def test_get_skip_accounts(self): + skipped = aws_utils.get_skip_accounts() + self.assertEqual(skipped, ['123', '456']) + + @patch.dict(os.environ, {'SKIP_ACCOUNTS': ''}) + def test_get_skip_accounts_empty(self): + skipped = aws_utils.get_skip_accounts() + self.assertEqual(skipped, []) + + @patch.dict(os.environ, {'OU_IDS': 'ou-1, ou-2'}) + def test_get_ou_ids(self): + ous = aws_utils.get_ou_ids() + self.assertEqual(ous, ['ou-1', 'ou-2']) + + @patch.dict(os.environ, {'OU_IDS': ''}) + def test_get_ou_ids_empty(self): + ous = aws_utils.get_ou_ids() + self.assertEqual(ous, []) + + @patch('aws_utils.get_boto_session') + def test_get_account_names(self, mock_get_session): + # Mock session and client + mock_session = MagicMock() + mock_client = MagicMock() + mock_get_session.return_value = mock_session + mock_session.client.return_value = mock_client + + # Mock pagination + mock_paginator = MagicMock() + mock_client.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + { + 'Accounts': [ + {'Id': '111', 'Name': 'Acc1', 'Status': 'ACTIVE'}, + {'Id': '222', 'Name': 'Acc2', 'Status': 'SUSPENDED'}, + {'Id': '333', 'Name': 'Acc3', 'Status': 'ACTIVE'} + ] + } + ] + + # Mock skip accounts to be empty for this test + with patch('aws_utils.get_skip_accounts', return_value=[]): + accounts = aws_utils.get_account_names() + + self.assertEqual(len(accounts), 2) + self.assertEqual(accounts['111'], 'Acc1') + self.assertEqual(accounts['333'], 'Acc3') + # Ensure SUSPENDED account is ignored + self.assertNotIn('222', accounts) + + @patch('aws_utils.get_boto_session') + def test_get_account_names_with_skip(self, mock_get_session): + mock_session = MagicMock() + mock_client = MagicMock() + mock_get_session.return_value = mock_session + mock_session.client.return_value = mock_client + + mock_paginator = MagicMock() + mock_client.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + { + 'Accounts': [ + {'Id': '111', 'Name': 'Acc1', 'Status': 'ACTIVE'}, + {'Id': '222', 'Name': 'Acc2', 'Status': 'ACTIVE'} + ] + } + ] + + with patch('aws_utils.get_skip_accounts', return_value=['222']): + accounts = aws_utils.get_account_names() + + self.assertEqual(len(accounts), 1) + self.assertEqual(accounts['111'], 'Acc1') + self.assertNotIn('222', accounts) + + @patch('aws_utils.get_boto_session') + def test_setup_org_accounts_session_explicit_ou(self, mock_get_session): + mock_session = MagicMock() + mock_org_client = MagicMock() + mock_get_session.return_value = mock_session + mock_session.client.return_value = mock_org_client + + mock_paginator = MagicMock() + mock_org_client.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + { + 'Accounts': [ + {'Id': '111', 'Name': 'Acc 1', 'Status': 'ACTIVE'} + ] + } + ] + + with patch('aws_utils.get_skip_accounts', return_value=[]): + # Iterate through generator + results = list(aws_utils.setup_org_accounts_session(ou_ids=['ou-test'])) + + self.assertEqual(len(results), 1) + account, profile = results[0] + self.assertEqual(account['Id'], '111') + self.assertEqual(profile, 'Acc-1.admin') # Checks sanitization of name + + @patch('aws_utils.get_boto_session') + def test_setup_org_accounts_session_fallback(self, mock_get_session): + mock_session = MagicMock() + mock_org_client = MagicMock() + mock_get_session.return_value = mock_session + mock_session.client.return_value = mock_org_client + + mock_paginator = MagicMock() + mock_org_client.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + { + 'Accounts': [ + {'Id': '999', 'Name': 'FallbackAcc', 'Status': 'ACTIVE'} + ] + } + ] + + with patch('aws_utils.get_ou_ids', return_value=[]), \ + patch('aws_utils.get_skip_accounts', return_value=[]): + + results = list(aws_utils.setup_org_accounts_session()) + + self.assertEqual(len(results), 1) + account, profile = results[0] + self.assertEqual(account['Id'], '999') + # Check that it called list_accounts (fallback) not list_accounts_for_parent + mock_org_client.get_paginator.assert_called_with('list_accounts') + +if __name__ == '__main__': + unittest.main() commit 66846dbfe0bc655b0d7d2bc987fa684907971ee4 Author: bob walker Date: Thu Feb 12 17:57:43 2026 +0000 add GEMINI.md for project context and AI agent guidance diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..a9e1816 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,43 @@ +# Gemini CLI Project Context - AWS Organization Audit Scripts + +## Project Overview +This repository contains a collection of Python scripts for auditing AWS Organizations. It handles multi-account discovery, SSO authentication, and generates reports on costs, bandwidth, and resource inventory. + +## Core Guidelines + +- **Shared Logic:** All AWS interaction MUST go through `aws_utils.py`. Never initialize `boto3.client` directly without first ensuring a session via `aws_utils`. +- **Authentication:** Use `ensure_sso_login()` to handle AWS SSO sessions. This triggers `aws sso login` via the CLI if the session is expired. +- **Session Management:** + - Use `get_boto_session()` for parent/management account tasks (Organizations, Pricing API). + - Use `setup_org_accounts_session()` to iterate through accounts in specific OUs or the entire organization. This handles profile switching for multi-account scans. +- **Configuration:** + - Honor environment variables: `AWS_PARENT_PROFILE`, `OU_IDS`, and `SKIP_ACCOUNTS`. + - Support `.env` files via `python-dotenv` (handled in `aws_utils.py`). +- **Reporting:** + - For large datasets, prefer CSV output using the `csv` module. + - For console summaries, use formatted tables with `print` or `pandas`. + +## Key Patterns + +### Account Iteration +Always use the generator pattern for cross-account audits: +```python +from aws_utils import setup_org_accounts_session +for account, profile_name in setup_org_accounts_session(): + boto3.setup_default_session(profile_name=profile_name) + # ... account-specific logic ... +``` + +### Date Handling +Standardize billing periods using `aws_utils.py` helpers: +- `get_previous_month_range()`: Returns `(start, end)` for the last full month. +- `get_last_n_months_ranges(n)`: Returns list of ranges for historical analysis. + +## Tooling & Dependencies +- **Primary:** `boto3`, `pandas`, `python-dotenv`. +- **Secondary:** `gspread` (for Google Sheets reports). +- **Environment:** Always assume execution within a virtual environment (`.venv`). + +## Security & Privacy +- **Identifiers:** Never hardcode Account IDs, OU IDs, or Profile Names. Use the configuration patterns in `aws_utils`. +- **Sanitization:** When creating examples or documentation, use generic placeholders like `ou-1234-abcd5678` or `123456789012`. commit ae687ab1691e832df419baaef684c001fbc5eb0b Author: bob walker Date: Thu Feb 12 17:00:57 2026 +0000 fix: use get_boto_session to ensure clean environment and correct profile usage diff --git a/.env b/.env new file mode 100644 index 0000000..5fd702e --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +# Parent AWS profile for Organizations and SSO login +AWS_PARENT_PROFILE="dxw.admin" + +# AWS Organization Unit IDs to scan (comma-separated) +# OU_IDS=ou-1234-abcd5678,ou-9012-efgh3456 + +# AWS Account IDs to skip (comma-separated) +# SKIP_ACCOUNTS=123456789012,987654321098 commit add381d68cd8bfa00de88e422a2afb734ada1a8b Author: bob walker Date: Thu Feb 12 16:44:15 2026 +0000 sanitize codebase by removing specific AWS identifiers and using generic placeholders diff --git a/cloudfront.py b/cloudfront.py index 50167a1..3c5adc4 100755 --- a/cloudfront.py +++ b/cloudfront.py @@ -1,113 +1,64 @@ import boto3 -from datetime import datetime, timedelta +import csv from botocore.exceptions import ClientError, ProfileNotFound +from aws_utils import setup_org_accounts_session, ensure_sso_login -# Function to list CloudFront distributions and associated domains -def list_cloudfront_distributions(cloudfront_client): - distributions_info = [] - paginator = cloudfront_client.get_paginator('list_distributions') - for page in paginator.paginate(): - for distribution in page['DistributionList'].get('Items', []): - distribution_id = distribution['Id'] - distribution_arn = f"arn:aws:cloudfront::{boto3.client('sts').get_caller_identity()['Account']}:distribution/{distribution_id}" - distribution_domains = distribution.get('Aliases', {}).get('Items', []) - distributions_info.append({ - 'DistributionId': distribution_id, - 'DistributionArn': distribution_arn, - 'Domains': distribution_domains - }) - return distributions_info - -# Function to get bandwidth cost and usage for a given distribution ARN -def get_bandwidth_cost_and_usage(cost_explorer_client, distribution_arn, start_date, end_date): +def get_cloudfront_details(account_id, account_name, cloudfront_client): + """ + Lists all CloudFront distributions for the account and determines + their associated details. + """ + dist_info_list = [] try: - response = cost_explorer_client.get_cost_and_usage( - TimePeriod={ - 'Start': start_date, - 'End': end_date - }, - Granularity='DAILY', - Metrics=['UnblendedCost', 'UsageQuantity'], - Filter={ - 'And': [ - {'Dimensions': {'Key': 'SERVICE', 'Values': ['Amazon CloudFront']}}, - {'Dimensions': {'Key': 'LINKED_ACCOUNT', 'Values': [boto3.client('sts').get_caller_identity()['Account']]}}, - {'Dimensions': {'Key': 'RESOURCE_ARN', 'Values': [distribution_arn]}} - ] - } - ) - - cost = response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'] - usage = response['ResultsByTime'][0]['Total']['UsageQuantity']['Amount'] - return cost, usage - - except Exception as e: - print(f"Error retrieving cost and usage for distribution {distribution_arn}: {e}") - return 'N/A', 'N/A' - -# Initialize AWS Organizations client -org_client = boto3.client('organizations') - -# List of specific Organizational Units (OUs) -ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] -active_accounts = [] - -# Get all active accounts for the specified OUs, handling pagination -for ou_id in ou_ids: - next_token = None - while True: - if next_token: - response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) - else: - response = org_client.list_accounts_for_parent(ParentId=ou_id) - - active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) - - next_token = response.get('NextToken') - if not next_token: - break + paginator = cloudfront_client.get_paginator('list_distributions') + for page in paginator.paginate(): + if 'DistributionList' in page and 'Items' in page['DistributionList']: + for dist in page['DistributionList']['Items']: + dist_info_list.append({ + 'AccountName': account_name, + 'AccountID': account_id, + 'ID': dist['Id'], + 'DomainName': dist['DomainName'], + 'Status': dist['Status'], + 'Enabled': dist['Enabled'], + 'Comment': dist.get('Comment', 'N/A') + }) + except ClientError as e: + print(f" -> Error listing distributions: {e}") + return dist_info_list -# Calculate the date range for the last 7 days -end_date = datetime.now().strftime('%Y-%m-%d') -start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') +def main(): + if not ensure_sso_login(): + return -# Set up the default session for the billing account ('dxw.admin') to access cost data -boto3.setup_default_session(profile_name='dxw.admin') -cost_explorer_client = boto3.client('ce') - -# Iterate through active accounts and list CloudFront distributions -for account in active_accounts: - account_id = account['Id'] - - # Replace " - " with "-" and replace spaces with "-" in account names - account_name = account['Name'].replace(" - ", "-").replace(" ", "-") - profile_name = f'{account_name}.admin' # Constructing the profile name + all_dist_info = [] + csv_filepath = 'cloudfront_distributions_info.csv' - try: - # Set up the default session using the AWS SSO profile for each account - boto3.setup_default_session(profile_name=profile_name) - - # Initialize CloudFront client with the SSO session - cloudfront_client = boto3.client('cloudfront') - - # Get CloudFront distribution info - distributions_info = list_cloudfront_distributions(cloudfront_client) - - # Get bandwidth cost/usage for each distribution using the ARN from the billing account - for dist in distributions_info: - distribution_id = dist['DistributionId'] - distribution_arn = dist['DistributionArn'] - domains = dist['Domains'] if dist['Domains'] else ['No custom domains'] - - # Retrieve the bandwidth cost and usage from the billing account session - cost, usage = get_bandwidth_cost_and_usage(cost_explorer_client, distribution_arn, start_date, end_date) + with open(csv_filepath, mode='w', newline='') as csvfile: + fieldnames = ['AccountName', 'AccountID', 'ID', 'DomainName', 'Status', 'Enabled', 'Comment'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + + print("\nIterating through accounts to collect CloudFront data...") + for account, profile_name in setup_org_accounts_session(): + account_id = account['Id'] + account_name = account['Name'] + print(f"\n--- Checking Account: {account_name} (ID: {account_id}) ---") - for domain in domains: - print(f'"{account_name}":"{distribution_id}":"{domain}":"Cost={cost}":"Usage={usage} GB"') - - print(f"Processed account '{account_name}' (ID: {account_id})") + try: + boto3.setup_default_session(profile_name=profile_name) + cloudfront_client = boto3.client('cloudfront') + + dist_data = get_cloudfront_details(account_id, account_name, cloudfront_client) + for dist in dist_data: + all_dist_info.append(dist) + writer.writerow(dist) + print(f" [FOUND] ID: {dist['ID']}, Domain: {dist['DomainName']}, Status: {dist['Status']}") + except (ClientError, ProfileNotFound) as e: + print(f" -> Error setting up session/profile '{profile_name}': {e}") - except (ClientError, ProfileNotFound) as e: - print(f"Error accessing account '{account_name}' (ID: {account_id}): {e}") + print(f"\nTotal distributions found: {len(all_dist_info)}") + print(f"CSV file '{csv_filepath}' generated successfully.") -print("\nCloudFront distributions, associated domains, and bandwidth cost/usage have been successfully listed for the last 7 days.") +if __name__ == "__main__": + main() commit e34ae042da6edccd2539f05d458df599d3fcdb1a Author: bob walker Date: Thu Feb 12 16:34:17 2026 +0000 add .env.example with configuration placeholders diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c7b9c36 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# AWS Organization Unit IDs to scan (comma-separated) +# OU_IDS=ou-h0vo-0waugyoz,ou-h0vo-32wgvd6w + +# AWS Account IDs to skip (comma-separated) +# SKIP_ACCOUNTS=123456789012,987654321098 commit 6ecf45a5989137cf1633bd3aff59440917e11d57 Author: bob walker Date: Thu Feb 12 16:33:02 2026 +0000 remove default OU IDs from aws_utils.py diff --git a/aws_utils.py b/aws_utils.py index 6878d30..ac60359 100644 --- a/aws_utils.py +++ b/aws_utils.py @@ -19,12 +19,11 @@ def get_skip_accounts(): return [s.strip() for s in skip_str.split(',') if s.strip()] def get_ou_ids(): - """Returns a list of OU IDs from OU_IDS env var, or defaults.""" + """Returns a list of OU IDs from OU_IDS env var.""" ou_str = os.getenv('OU_IDS', '') if ou_str: return [o.strip() for o in ou_str.split(',') if o.strip()] - # Default OUs if none provided - return ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] + return [] def get_account_names(): """Fetches account names from AWS Organizations, excluding skipped accounts.""" commit b6322a103ee83d9f8d10c31e446bdd9949132949 Author: bob walker Date: Thu Feb 12 16:30:25 2026 +0000 make OUs and skipped accounts configurable via env vars diff --git a/aws-asg-lt-ami.py b/aws-asg-lt-ami.py index 0936d9c..d36140c 100755 --- a/aws-asg-lt-ami.py +++ b/aws-asg-lt-ami.py @@ -4,6 +4,7 @@ import csv import time from botocore.exceptions import ClientError, ProfileNotFound from collections import defaultdict +from aws_utils import setup_org_accounts_session # The original script's helper function to get an AMI's human-readable name from its ID def get_ami_name(ec2_client, ami_id): @@ -127,30 +128,6 @@ def get_asg_details(account_id, account_name, region, asg_client, ec2_client): return asg_info_list # Initialize AWS Organizations client -# NOTE: This part is taken directly from your example script's organizational structure. -org_client = boto3.client('organizations') - -# List of specific Organizational Units (OUs) to check -ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] -active_accounts = [] - -# Get all active accounts for the specified OUs, handling pagination -print("1. Fetching active accounts from specified OUs...") -for ou_id in ou_ids: - next_token = None - while True: - if next_token: - response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) - else: - response = org_client.list_accounts_for_parent(ParentId=ou_id) - - active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) - - next_token = response.get('NextToken') - if not next_token: - break -print(f"-> Found {len(active_accounts)} active accounts to check.") - all_asg_info = [] # Define the regions to check (matches your example script) @@ -175,12 +152,9 @@ with open(csv_filepath, mode='w', newline='') as csvfile: # Iterate through active accounts and then through the regions print("\n2. Iterating through accounts and regions to collect ASG data...") - for account in active_accounts: + for account, profile_name in setup_org_accounts_session(): account_id = account['Id'] - # Sanitize account name for profile use (matching original script's logic) - account_name = account['Name'].replace(" - ", "-").replace(" ", "-") - profile_name = f'{account_name}.admin' - + account_name = account['Name'] print(f"\n--- Checking Account: {account_name} (ID: {account_id}) ---") try: diff --git a/aws-ec2-ami.py b/aws-ec2-ami.py index cbdf8cd..81e4f86 100755 --- a/aws-ec2-ami.py +++ b/aws-ec2-ami.py @@ -3,6 +3,7 @@ import boto3 import csv from botocore.exceptions import ClientError, ProfileNotFound from collections import defaultdict +from aws_utils import setup_org_accounts_session # Function to get the instance name from tags def get_instance_name(tags): @@ -47,28 +48,6 @@ def list_running_ec2_instances_with_ami_info(ec2_client): }) return instances_info -# Initialize AWS Organizations client -org_client = boto3.client('organizations') - -# List of specific Organizational Units (OUs) -ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] -active_accounts = [] - -# Get all active accounts for the specified OUs, handling pagination -for ou_id in ou_ids: - next_token = None - while True: - if next_token: - response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) - else: - response = org_client.list_accounts_for_parent(ParentId=ou_id) - - active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) - - next_token = response.get('NextToken') - if not next_token: - break - # Initialize a dictionary to store AMI names and their counts ami_name_counts = defaultdict(int) all_instances_info = [] @@ -84,13 +63,9 @@ with open('running_ec2_instances_info.csv', mode='w', newline='') as csvfile: writer.writeheader() # Iterate through active accounts and then through the regions - for account in active_accounts: + for account, profile_name in setup_org_accounts_session(): account_id = account['Id'] - - # Replace " - " with "-" and replace spaces with "-" in account names - account_name = account['Name'].replace(" - ", "-").replace(" ", "-") - profile_name = f'{account_name}.admin' # Constructing the profile name - + account_name = account['Name'] try: # Set up the default session using the AWS SSO profile for each account boto3.setup_default_session(profile_name=profile_name) diff --git a/aws-ips.py b/aws-ips.py index 5a686a0..a7b2f5a 100644 --- a/aws-ips.py +++ b/aws-ips.py @@ -1,6 +1,7 @@ import boto3 import csv from botocore.exceptions import ClientError, ProfileNotFound +from aws_utils import setup_org_accounts_session # Function to list public IPs, Private IPs, and Instance IDs def list_ec2_instance_info(ec2_client): @@ -40,28 +41,6 @@ def list_alb_dns_names(elbv2_client): alb_dns_names.append(lb['DNSName']) return alb_dns_names -# Initialize AWS Organizations client -org_client = boto3.client('organizations') - -# List of specific Organizational Units (OUs) -ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] -active_accounts = [] - -# Get all active accounts for the specified OUs, handling pagination -for ou_id in ou_ids: - next_token = None - while True: - if next_token: - response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) - else: - response = org_client.list_accounts_for_parent(ParentId=ou_id) - - active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) - - next_token = response.get('NextToken') - if not next_token: - break - # Initialize a counter for the total number of external resources total_external_resources_count = 0 @@ -85,11 +64,9 @@ with open('aws_external_ips_by_region.csv', mode='w', newline='') as csvfile: writer.writeheader() # Iterate through active accounts and then through the regions - for account in active_accounts: + for account, profile_name in setup_org_accounts_session(): account_id = account['Id'] - account_name = account['Name'].replace(" - ", "-").replace(" ", "-") - profile_name = f'{account_name}.admin' - + account_name = account['Name'] try: boto3.setup_default_session(profile_name=profile_name) diff --git a/aws-metrics.py b/aws-metrics.py index db72036..93174c2 100644 --- a/aws-metrics.py +++ b/aws-metrics.py @@ -2,6 +2,7 @@ import boto3 import csv from datetime import datetime, timedelta, timezone from botocore.exceptions import ClientError, ProfileNotFound +from aws_utils import setup_org_accounts_session # Function to get CloudFront log size for the last month def get_cloudfront_log_size(cloudfront_client, s3_client, distribution_id): @@ -65,48 +66,15 @@ def count_rds_clusters(rds_client): clusters = rds_client.describe_db_clusters() return len(clusters['DBClusters']) -# Initialize AWS Organizations client -org_client = boto3.client('organizations') - -# List of specific Organizational Units (OUs) -ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] -active_accounts = [] - -# Get all active accounts for the specified OUs, handling pagination -for ou_id in ou_ids: - next_token = None - while True: - if next_token: - response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) - else: - response = org_client.list_accounts_for_parent(ParentId=ou_id) - - active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) - - next_token = response.get('NextToken') - if not next_token: - break - -# Initialize total counters -total_accounts = 0 -total_ec2_instances = 0 -total_ecs_containers = 0 -total_rds_instances = 0 -total_rds_clusters = 0 -total_cloudfront_log_size_bytes = 0 - # Prepare CSV file with open('aws_account_metrics.csv', mode='w', newline='') as file: writer = csv.writer(file) writer.writerow(['Account ID', 'Account Email', 'EC2 Instance Count', 'ECS Running Containers', 'RDS Instances', 'RDS Clusters', 'CloudFront Log Size (GB)']) # Iterate through active accounts - for account in active_accounts: + for account, profile_name in setup_org_accounts_session(): account_id = account['Id'] - - # Replace " - " with "-" and replace spaces with "-" in account names - account_name = account['Name'].replace(" - ", "-").replace(" ", "-") - profile_name = f'{account_name}.admin' # Constructing the profile name + account_name = account['Name'] account_email = account['Email'] try: diff --git a/aws-vpc.py b/aws-vpc.py index f0b386e..c563722 100755 --- a/aws-vpc.py +++ b/aws-vpc.py @@ -23,7 +23,6 @@ def list_vpcs(ec2_client): return vpcs_info def main(): - ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] regions_to_check = ['eu-west-1', 'eu-west-2'] total_vpcs_all_accounts = 0 @@ -32,7 +31,9 @@ def main(): writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() - for account_id, account_name, profile_name in setup_org_accounts_session(ou_ids): + for account, profile_name in setup_org_accounts_session(): + account_id = account['Id'] + account_name = account['Name'] try: boto3.setup_default_session(profile_name=profile_name) for region in regions_to_check: diff --git a/aws_utils.py b/aws_utils.py index e129cb0..6878d30 100644 --- a/aws_utils.py +++ b/aws_utils.py @@ -1,17 +1,41 @@ import boto3 import json +import os from datetime import datetime, date from dateutil.relativedelta import relativedelta +# Attempt to load .env file if python-dotenv is installed +try: + from dotenv import load_dotenv + load_dotenv() +except ImportError: + pass + +def get_skip_accounts(): + """Returns a list of account IDs to skip from SKIP_ACCOUNTS env var.""" + skip_str = os.getenv('SKIP_ACCOUNTS', '') + if not skip_str: + return [] + return [s.strip() for s in skip_str.split(',') if s.strip()] + +def get_ou_ids(): + """Returns a list of OU IDs from OU_IDS env var, or defaults.""" + ou_str = os.getenv('OU_IDS', '') + if ou_str: + return [o.strip() for o in ou_str.split(',') if o.strip()] + # Default OUs if none provided + return ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] + def get_account_names(): - """Fetches account names from AWS Organizations.""" + """Fetches account names from AWS Organizations, excluding skipped accounts.""" org_client = boto3.client('organizations') + skip_accounts = get_skip_accounts() accounts = {} try: paginator = org_client.get_paginator('list_accounts') for page in paginator.paginate(): for account in page['Accounts']: - if account['Status'] == 'ACTIVE': + if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts: accounts[account['Id']] = account['Name'] except Exception as e: print(f"Error fetching account names: {e}") @@ -59,19 +83,23 @@ def get_aws_pricing(service_code, filters): print(f"Error fetching pricing for {service_code}: {e}") return None -def setup_org_accounts_session(ou_ids, profile_suffix='.admin'): +def setup_org_accounts_session(ou_ids=None, profile_suffix='.admin'): """ - Yields (account_id, account_name, profile_name) for active accounts in OUs. + Yields (account_dict, profile_name) for active accounts in OUs. Handles boto3 session setup for each account. + Excludes accounts in SKIP_ACCOUNTS env var. """ + if ou_ids is None: + ou_ids = get_ou_ids() + + skip_accounts = get_skip_accounts() org_client = boto3.client('organizations') for ou_id in ou_ids: paginator = org_client.get_paginator('list_accounts_for_parent') for page in paginator.paginate(ParentId=ou_id): for account in page['Accounts']: - if account['Status'] == 'ACTIVE': - account_id = account['Id'] + if account['Status'] == 'ACTIVE' and account['Id'] not in skip_accounts: # Sanitize account name for profile use account_name = account['Name'].replace(" - ", "-").replace(" ", "-") profile_name = f'{account_name}{profile_suffix}' - yield account_id, account['Name'], profile_name + yield account, profile_name diff --git a/list_rds_instances.py b/list_rds_instances.py index 2e14f74..2d56934 100644 --- a/list_rds_instances.py +++ b/list_rds_instances.py @@ -19,9 +19,9 @@ def list_rds_clusters(rds_client): return cluster_names def main(): - ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] - - for account_id, account_name, profile_name in setup_org_accounts_session(ou_ids): + for account, profile_name in setup_org_accounts_session(): + account_id = account['Id'] + account_name = account['Name'] try: boto3.setup_default_session(profile_name=profile_name) rds_client = boto3.client('rds') commit dc9e82b6ee010d4cc252953faa930655e3be55fe Author: bob walker Date: Thu Feb 12 16:19:08 2026 +0000 add script to list RDS instances and clusters across accounts diff --git a/list_rds_instances.py b/list_rds_instances.py new file mode 100644 index 0000000..0db91c3 --- /dev/null +++ b/list_rds_instances.py @@ -0,0 +1,78 @@ +import boto3 +from botocore.exceptions import ClientError, ProfileNotFound + +# Function to list RDS instances +def list_rds_instances(rds_client): + instance_names = [] + paginator = rds_client.get_paginator('describe_db_instances') + for page in paginator.paginate(): + for db_instance in page['DBInstances']: + instance_names.append(db_instance['DBInstanceIdentifier']) + return instance_names + +# Function to list RDS clusters +def list_rds_clusters(rds_client): + cluster_names = [] + paginator = rds_client.get_paginator('describe_db_clusters') + for page in paginator.paginate(): + for db_cluster in page['DBClusters']: + cluster_names.append(db_cluster['DBClusterIdentifier']) + return cluster_names + +# Initialize AWS Organizations client +org_client = boto3.client('organizations') + +# List of specific Organizational Units (OUs) +ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] +active_accounts = [] + +# Get all active accounts for the specified OUs, handling pagination +for ou_id in ou_ids: + next_token = None + while True: + if next_token: + response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) + else: + response = org_client.list_accounts_for_parent(ParentId=ou_id) + + active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) + + next_token = response.get('NextToken') + if not next_token: + break + +# Iterate through active accounts and print RDS names +for account in active_accounts: + account_id = account['Id'] + + # Replace " - " with "-" and replace spaces with "-" in account names + account_name = account['Name'].replace(" - ", "-").replace(" ", "-") + profile_name = f'{account_name}.admin' # Constructing the profile name + + try: + # Set up the default session using the AWS SSO profile for each account + boto3.setup_default_session(profile_name=profile_name) + + # Initialize RDS client with the SSO session + rds_client = boto3.client('rds') + + # Get RDS instance names + rds_instance_names = list_rds_instances(rds_client) + + # Get RDS cluster names + rds_cluster_names = list_rds_clusters(rds_client) + + # Print RDS instance names + for rds_name in rds_instance_names: + print(f'"{account_name}":"{rds_name}"') + + # Print RDS cluster names + for rds_name in rds_cluster_names: + print(f'"{account_name}":"{rds_name}"') + + print(f"Processed account '{account_name}' (ID: {account_id})") + + except (ClientError, ProfileNotFound) as e: + print(f"Error accessing account '{account_name}' (ID: {account_id}): {e}") + +print("\nRDS names have been successfully listed for all accounts.") commit ffecb6b4b28850b96ad9f5718458fc104d892409 Author: bob walker Date: Thu Feb 12 16:18:20 2026 +0000 add script to list VPCs and their CIDR blocks across accounts diff --git a/aws-vpc.py b/aws-vpc.py new file mode 100755 index 0000000..f771eb1 --- /dev/null +++ b/aws-vpc.py @@ -0,0 +1,122 @@ +import boto3 +import csv +from botocore.exceptions import ClientError, ProfileNotFound + +# Function to get the VPC name from tags +def get_vpc_name(tags): + """ + Retrieves the value of the 'Name' tag from a VPC's tags. + Returns "Unnamed VPC" if the tag is not found. + """ + if tags: + for tag in tags: + if tag['Key'] == 'Name': + return tag['Value'] + return "Unnamed VPC" + +# Function to list all VPCs with name, ID, and CIDR block +def list_vpcs(ec2_client): + """ + Lists all VPCs in a given region, retrieves their Name, ID, + and CIDR block, and returns the information in a list of dictionaries. + """ + vpcs_info = [] + paginator = ec2_client.get_paginator('describe_vpcs') + + for page in paginator.paginate(): + for vpc in page['Vpcs']: + vpc_name = get_vpc_name(vpc.get('Tags')) + vpc_id = vpc['VpcId'] + cidr_block = vpc.get('CidrBlock', 'N/A') + + vpcs_info.append({ + 'VPCName': vpc_name, + 'VPCID': vpc_id, + 'CIDRBlock': cidr_block + }) + return vpcs_info + +# Initialize AWS Organizations client +org_client = boto3.client('organizations') + +# List of specific Organizational Units (OUs) +ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] +active_accounts = [] + +# Get all active accounts for the specified OUs, handling pagination +for ou_id in ou_ids: + next_token = None + while True: + if next_token: + response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) + else: + response = org_client.list_accounts_for_parent(ParentId=ou_id) + + active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) + + next_token = response.get('NextToken') + if not next_token: + break + +# Define the regions to check +regions_to_check = ['eu-west-1', 'eu-west-2'] + +# Initialize a counter for the total number of VPCs +total_vpcs_all_accounts = 0 + +# Open CSV file for writing VPC information +with open('aws_vpcs_by_region.csv', mode='w', newline='') as csvfile: + # Define the fieldnames for the CSV header + fieldnames = ['AccountName', 'AccountID', 'Region', 'VPCName', 'VPCID', 'CIDRBlock'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + + # Iterate through active accounts and then through the regions + for account in active_accounts: + account_id = account['Id'] + + # Replace " - " with "-" and replace spaces with "-" in account names + account_name = account['Name'].replace(" - ", "-").replace(" ", "-") + profile_name = f'{account_name}.admin' # Constructing the profile name + + try: + # Set up the default session using the AWS SSO profile for each account + boto3.setup_default_session(profile_name=profile_name) + + for region in regions_to_check: + try: + # Initialize EC2 client for the specific region + ec2_client = boto3.client('ec2', region_name=region) + + # List all VPCs for the account and region + vpcs_info = list_vpcs(ec2_client) + + # Add the count for this region to the total + total_vpcs_all_accounts += len(vpcs_info) + + # Write VPC information to CSV + for vpc in vpcs_info: + writer.writerow({ + 'AccountName': account_name, + 'AccountID': account_id, + 'Region': region, + 'VPCName': vpc['VPCName'], + 'VPCID': vpc['VPCID'], + 'CIDRBlock': vpc['CIDRBlock'] + }) + + # Print the results for the current account and region + print(f'"{account_name}" (ID: {account_id}, Region: {region}) VPCs:') + for vpc in vpcs_info: + print(f"VPC Name: {vpc['VPCName']}, VPC ID: {vpc['VPCID']}, CIDR Block: {vpc['CIDRBlock']}") + print('--------------------------------------------------------') + + except ClientError as e: + print(f"Error accessing VPCs in region '{region}' for account '{account_name}' (ID: {account_id}): {e}") + + except (ClientError, ProfileNotFound) as e: + print(f"Error accessing account '{account_name}' (ID: {account_id}): {e}") + +print("\nSummary of VPCs Across All Accounts and Regions:") +print(f"Total number of VPCs found: {total_vpcs_all_accounts}") +print("\nCSV file 'aws_vpcs_by_region.csv' generated successfully.") commit 1e3cbc227fba888f464c58a151f32d9b6e329b37 Author: bob walker Date: Thu Feb 12 16:18:08 2026 +0000 add script to audit Auto Scaling Groups and their AMIs across accounts diff --git a/aws-asg-lt-ami.py b/aws-asg-lt-ami.py new file mode 100755 index 0000000..0936d9c --- /dev/null +++ b/aws-asg-lt-ami.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +import boto3 +import csv +import time +from botocore.exceptions import ClientError, ProfileNotFound +from collections import defaultdict + +# The original script's helper function to get an AMI's human-readable name from its ID +def get_ami_name(ec2_client, ami_id): + """ + Fetches the AMI Name from the AMI ID using the EC2 client. + Handles potential errors if the AMI is not accessible or deleted. + """ + if not ami_id: + return "N/A - No AMI ID" + # Skip AMI lookup if the ID is clearly an error message from a previous step + if not ami_id.startswith('ami-'): + return f"Invalid AMI ID format" + + try: + # Use describe_images to get metadata about the AMI + image_info = ec2_client.describe_images(ImageIds=[ami_id])['Images'] + if image_info: + return image_info[0].get('Name', 'Unnamed AMI') + return 'AMI not found' + except ClientError as e: + # Handle cases where the AMI is not found or permissions are denied + return f"AMI not accessible (Error: {e.response['Error']['Code']})" + except Exception as e: + return f"AMI lookup error: {e}" + + +def get_asg_details(account_id, account_name, region, asg_client, ec2_client): + """ + Lists all Auto Scaling Groups (ASGs) in the region and determines + their associated AMI ID, Launch Configuration, or Launch Template details. + """ + asg_info_list = [] + paginator = asg_client.get_paginator('describe_auto_scaling_groups') + + print(f" -> Fetching ASGs in {region}...") + + try: + for page in paginator.paginate(): + for asg in page['AutoScalingGroups']: + ami_id = "N/A" + template_type = "None" + template_name = "None" + template_version = "N/A" + + # 1. Check for Launch Template + lt_data = asg.get('LaunchTemplate') or asg.get('MixedInstancesPolicy', {}).get('LaunchTemplate') + + if lt_data: + # An ASG can use either a primary LaunchTemplate or a MixedInstancesPolicy + template_type = "LaunchTemplate" + template_id = lt_data.get('LaunchTemplateId') + template_name = lt_data.get('LaunchTemplateName') + template_version = lt_data.get('Version') or "$Default" # Use $Default if version is missing + + if template_id or template_name: + # Attempt to resolve the AMI from the Launch Template version + try: + # AWS API requires EITHER LaunchTemplateId OR LaunchTemplateName, not both. + lt_lookup_args = {'Versions': [template_version]} + + if template_id: + lt_lookup_args['LaunchTemplateId'] = template_id + elif template_name: + lt_lookup_args['LaunchTemplateName'] = template_name + else: + # This case is unlikely if lt_data was present, but provides robust error handling + raise ValueError("Launch Template ID and Name are both missing.") + + lt_versions = ec2_client.describe_launch_template_versions(**lt_lookup_args)['LaunchTemplateVersions'] + + if lt_versions: + # AMI ID is nested inside LaunchTemplateData + launch_template_data = lt_versions[0]['LaunchTemplateData'] + ami_id = launch_template_data.get('ImageId', 'AMI ID not specified in LT') + + except ClientError as e: + # Capture the specific error code for debugging + ami_id = f"LT lookup error: {e.response['Error']['Code']}" + except Exception as e: + ami_id = f"LT processing error: {e}" + + # 2. Check for Launch Configuration (Legacy) + elif asg.get('LaunchConfigurationName'): + lc_name = asg['LaunchConfigurationName'] + template_type = "LaunchConfiguration" + template_name = lc_name + template_version = "Latest" # LCs don't have versions like LTs + + # Attempt to resolve the AMI from the Launch Configuration + try: + lc_response = asg_client.describe_launch_configurations( + LaunchConfigurationNames=[lc_name] + )['LaunchConfigurations'] + + if lc_response: + ami_id = lc_response[0].get('ImageId', 'AMI ID not specified in LC') + + except ClientError as e: + ami_id = f"LC lookup error: {e.response['Error']['Code']}" + except Exception as e: + ami_id = f"LC processing error: {e}" + + # Resolve the AMI Name if an ID was found + ami_name = get_ami_name(ec2_client, ami_id) + + asg_info_list.append({ + 'AccountName': account_name, + 'AccountID': account_id, + 'Region': region, + 'ASG_Name': asg['AutoScalingGroupName'], + 'Template_Type': template_type, + 'Template_Name': template_name, + 'Template_Version': template_version, + 'AMI_ID': ami_id, + 'AMI_Name': ami_name, + }) + + except ClientError as e: + print(f" -> Error listing ASGs in {region}: {e}") + + return asg_info_list + +# Initialize AWS Organizations client +# NOTE: This part is taken directly from your example script's organizational structure. +org_client = boto3.client('organizations') + +# List of specific Organizational Units (OUs) to check +ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] +active_accounts = [] + +# Get all active accounts for the specified OUs, handling pagination +print("1. Fetching active accounts from specified OUs...") +for ou_id in ou_ids: + next_token = None + while True: + if next_token: + response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) + else: + response = org_client.list_accounts_for_parent(ParentId=ou_id) + + active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) + + next_token = response.get('NextToken') + if not next_token: + break +print(f"-> Found {len(active_accounts)} active accounts to check.") + +all_asg_info = [] + +# Define the regions to check (matches your example script) +regions_to_check = ['eu-west-1', 'eu-west-2'] + +# Open CSV file for writing ASG information +csv_filepath = 'autoscaling_group_ami_template_info.csv' +with open(csv_filepath, mode='w', newline='') as csvfile: + fieldnames = [ + 'AccountName', + 'AccountID', + 'Region', + 'ASG_Name', + 'Template_Type', + 'Template_Name', + 'Template_Version', + 'AMI_ID', + 'AMI_Name' + ] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + + # Iterate through active accounts and then through the regions + print("\n2. Iterating through accounts and regions to collect ASG data...") + for account in active_accounts: + account_id = account['Id'] + # Sanitize account name for profile use (matching original script's logic) + account_name = account['Name'].replace(" - ", "-").replace(" ", "-") + profile_name = f'{account_name}.admin' + + print(f"\n--- Checking Account: {account_name} (ID: {account_id}) ---") + + try: + # Set up the default session using the AWS SSO profile for each account + # NOTE: This relies on your local AWS configuration having these profiles set up. + boto3.setup_default_session(profile_name=profile_name) + + # Loop through each region + for region in regions_to_check: + try: + # Initialize clients for the specific region + asg_client = boto3.client('autoscaling', region_name=region) + ec2_client = boto3.client('ec2', region_name=region) # Needed to resolve AMI Name and LT details + + # Get ASG information + asg_data = get_asg_details(account_id, account_name, region, asg_client, ec2_client) + + # Write results to CSV and print to console + for asg in asg_data: + all_asg_info.append(asg) + writer.writerow(asg) + print(f" [FOUND] ASG: {asg['ASG_Name']}, Template Type: {asg['Template_Type']}, Template Name: {asg['Template_Name']} ({asg['Template_Version']}), AMI ID: {asg['AMI_ID']}, AMI Name: {asg['AMI_Name']}") + + except (ClientError) as e: + print(f" -> Error accessing services in region '{region}': {e}") + + # Small delay to respect API limits, especially for org-wide searches + time.sleep(0.5) + + except (ClientError, ProfileNotFound) as e: + print(f" -> Error setting up session/profile '{profile_name}': {e}") + + +# Output summary +total_asgs = len(all_asg_info) +print("\n========================================================") +print("ASG & AMI Collection Summary:") +print(f"Total Auto Scaling Groups found: {total_asgs}") + +if total_asgs > 0: + # Basic summary of AMI usage + ami_usage = defaultdict(int) + for asg in all_asg_info: + ami_usage[asg['AMI_Name']] += 1 + + print(f"Distinct AMI Names used: {len(ami_usage)}") + print("\nTop 5 AMI Usage Counts:") + for ami_name, count in sorted(ami_usage.items(), key=lambda item: item[1], reverse=True)[:5]: + print(f" - {ami_name}: {count} ASGs") + +print(f"\nCSV file '{csv_filepath}' generated successfully.") +print("========================================================") commit d26c4f2730c4d04a5f015b592681edc6f04d879e Author: bob walker Date: Tue Nov 5 15:24:46 2024 +0000 Script to list ami names diff --git a/aws-ec2-ami.py b/aws-ec2-ami.py new file mode 100644 index 0000000..392a856 --- /dev/null +++ b/aws-ec2-ami.py @@ -0,0 +1,100 @@ +import boto3 +from botocore.exceptions import ClientError, ProfileNotFound + +# Function to get the instance name from tags +def get_instance_name(tags): + if tags: + for tag in tags: + if tag['Key'] == 'Name': + return tag['Value'] + return "Unnamed" + +# Function to list EC2 instances with name, AMI ID, and AMI name +def list_ec2_instances_with_ami_info(ec2_client): + instances_info = [] + paginator = ec2_client.get_paginator('describe_instances') + for page in paginator.paginate(): + for reservation in page['Reservations']: + for instance in reservation['Instances']: + instance_name = get_instance_name(instance.get('Tags')) + ami_id = instance['ImageId'] + + # Fetch the AMI name using describe_images + try: + image_info = ec2_client.describe_images(ImageIds=[ami_id])['Images'][0] + ami_name = image_info.get('Name', 'Unnamed AMI') + except ClientError as e: + ami_name = 'AMI not accessible' # In case the AMI is private or not accessible + + instances_info.append({ + 'InstanceName': instance_name, + 'InstanceId': instance['InstanceId'], + 'AMI_ID': ami_id, + 'AMI_Name': ami_name + }) + return instances_info + +# Initialize AWS Organizations client +org_client = boto3.client('organizations') + +# List of specific Organizational Units (OUs) +ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] +active_accounts = [] + +# Get all active accounts for the specified OUs, handling pagination +for ou_id in ou_ids: + next_token = None + while True: + if next_token: + response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) + else: + response = org_client.list_accounts_for_parent(ParentId=ou_id) + + active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) + + next_token = response.get('NextToken') + if not next_token: + break + +# Initialize a dictionary to store all AMI names and instance information across all accounts +all_ami_names = set() +all_instances_info = [] + +# Iterate through active accounts and collect instance details +for account in active_accounts: + account_id = account['Id'] + + # Replace " - " with "-" and replace spaces with "-" in account names + account_name = account['Name'].replace(" - ", "-").replace(" ", "-") + profile_name = f'{account_name}.admin' # Constructing the profile name + + try: + # Set up the default session using the AWS SSO profile for each account + boto3.setup_default_session(profile_name=profile_name) + + # Initialize EC2 client with the SSO session + ec2_client = boto3.client('ec2') + + # List EC2 instances with name, AMI ID, and AMI name + instances_info = list_ec2_instances_with_ami_info(ec2_client) + + # Collect instance information and AMI names + for instance in instances_info: + all_instances_info.append(instance) + all_ami_names.add(instance['AMI_Name']) # Use a set to keep only unique AMI names + + # Print the results for the current account + print(f'"{account_name}" (ID: {account_id}) EC2 Instances:') + for instance in instances_info: + print(f"Instance Name: {instance['InstanceName']}, Instance ID: {instance['InstanceId']}, AMI ID: {instance['AMI_ID']}, AMI Name: {instance['AMI_Name']}") + print('--------------------------------------------------------') + + except (ClientError, ProfileNotFound) as e: + print(f"Error accessing account '{account_name}' (ID: {account_id}): {e}") + +# Output summary of distinct AMI names and total instance details +print("\nSummary of EC2 Instances Across All Accounts:") +print(f"Total number of distinct AMI names across all accounts: {len(all_ami_names)}") +print("List of distinct AMI names across all accounts:") +for ami_name in sorted(all_ami_names): + print(f"- {ami_name}") commit 8c3a6358c68d5f2bdddfb4702f152234e6a4cc8a Author: bob walker Date: Tue Oct 15 17:22:56 2024 +0100 Add aws-ips.py script to list external IPs of EC2 instances and Elastic IPs diff --git a/aws-ips.py b/aws-ips.py new file mode 100644 index 0000000..0bb35fb --- /dev/null +++ b/aws-ips.py @@ -0,0 +1,94 @@ +import boto3 +from botocore.exceptions import ClientError, ProfileNotFound + +# Function to list public IPs of EC2 instances +def list_ec2_public_ips(ec2_client): + public_ips = [] + paginator = ec2_client.get_paginator('describe_instances') + for page in paginator.paginate(): + for reservation in page['Reservations']: + for instance in reservation['Instances']: + if 'PublicIpAddress' in instance: + public_ips.append(instance['PublicIpAddress']) + return public_ips + +# Function to list Elastic IPs (EIPs) without pagination +def list_elastic_ips(ec2_client): + elastic_ips = [] + response = ec2_client.describe_addresses() + for address in response['Addresses']: + if 'PublicIp' in address: + elastic_ips.append(address['PublicIp']) + return elastic_ips + +# Initialize AWS Organizations client +org_client = boto3.client('organizations') + +# List of specific Organizational Units (OUs) +ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] +active_accounts = [] + +# Get all active accounts for the specified OUs, handling pagination +for ou_id in ou_ids: + next_token = None + while True: + if next_token: + response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) + else: + response = org_client.list_accounts_for_parent(ParentId=ou_id) + + active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) + + next_token = response.get('NextToken') + if not next_token: + break + +# Initialize a list to store all external IPs across all accounts +all_external_ips = [] + +# Initialize a counter for the total number of external IPs across all accounts +total_external_ips_all_accounts = 0 + +# Iterate through active accounts and count external IPs +for account in active_accounts: + account_id = account['Id'] + + # Replace " - " with "-" and replace spaces with "-" in account names + account_name = account['Name'].replace(" - ", "-").replace(" ", "-") + profile_name = f'{account_name}.admin' # Constructing the profile name + + try: + # Set up the default session using the AWS SSO profile for each account + boto3.setup_default_session(profile_name=profile_name) + + # Initialize EC2 client with the SSO session + ec2_client = boto3.client('ec2') + + # Get public IPs of EC2 instances + ec2_public_ips = list_ec2_public_ips(ec2_client) + + # Get Elastic IPs (EIPs) without pagination + elastic_ips = list_elastic_ips(ec2_client) + + # Combine EC2 public IPs and Elastic IPs into one list for the account + account_ips = ec2_public_ips + elastic_ips + + # Add account's IPs to the global list + all_external_ips.extend(account_ips) + + # Update the total count of external IPs for all accounts + total_external_ips_all_accounts += len(account_ips) + + # Print the results for the current account + print(f'"{account_name}" (ID: {account_id}) has {len(account_ips)} external IPs') + print(f'EC2 Public IPs: {ec2_public_ips}') + print(f'Elastic IPs: {elastic_ips}') + print('--------------------------------------------------------') + + except (ClientError, ProfileNotFound) as e: + print(f"Error accessing account '{account_name}' (ID: {account_id}): {e}") + +# Output the total number of external IPs for all accounts and the list of all external IPs +print("\nSummary of External IPs Across All Accounts:") +print(f"Total number of external IPs across all accounts: {total_external_ips_all_accounts}") +print(f"List of all external IPs across all accounts: {all_external_ips}") commit 79b83835e88a9b087f28ec11c41d63592e1dc1fe Author: bob walker Date: Fri Sep 13 11:34:23 2024 +0100 Add a script to list CloudFront distributions and associated domains for all accounts diff --git a/cloudfront.py b/cloudfront.py new file mode 100755 index 0000000..8e30703 --- /dev/null +++ b/cloudfront.py @@ -0,0 +1,70 @@ +import boto3 +from botocore.exceptions import ClientError, ProfileNotFound + +# Function to list CloudFront distributions and associated domains +def list_cloudfront_distributions(cloudfront_client): + distributions_info = [] + paginator = cloudfront_client.get_paginator('list_distributions') + for page in paginator.paginate(): + for distribution in page['DistributionList'].get('Items', []): + distribution_id = distribution['Id'] + distribution_domains = distribution.get('Aliases', {}).get('Items', []) + distributions_info.append({ + 'DistributionId': distribution_id, + 'Domains': distribution_domains + }) + return distributions_info + +# Initialize AWS Organizations client +org_client = boto3.client('organizations') + +# List of specific Organizational Units (OUs) +ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] +active_accounts = [] + +# Get all active accounts for the specified OUs, handling pagination +for ou_id in ou_ids: + next_token = None + while True: + if next_token: + response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) + else: + response = org_client.list_accounts_for_parent(ParentId=ou_id) + + active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) + + next_token = response.get('NextToken') + if not next_token: + break + +# Iterate through active accounts and list CloudFront distributions +for account in active_accounts: + account_id = account['Id'] + + # Replace " - " with "-" and replace spaces with "-" in account names + account_name = account['Name'].replace(" - ", "-").replace(" ", "-") + profile_name = f'{account_name}.admin' # Constructing the profile name + + try: + # Set up the default session using the AWS SSO profile for each account + boto3.setup_default_session(profile_name=profile_name) + + # Initialize CloudFront client with the SSO session + cloudfront_client = boto3.client('cloudfront') + + # Get CloudFront distribution info + distributions_info = list_cloudfront_distributions(cloudfront_client) + + # Print CloudFront distribution IDs and associated domains + for dist in distributions_info: + distribution_id = dist['DistributionId'] + domains = dist['Domains'] if dist['Domains'] else ['No custom domains'] + for domain in domains: + print(f'"{account_name}":"{distribution_id}":"{domain}"') + + print(f"Processed account '{account_name}' (ID: {account_id})") + + except (ClientError, ProfileNotFound) as e: + print(f"Error accessing account '{account_name}' (ID: {account_id}): {e}") + +print("\nCloudFront distributions and associated domains have been successfully listed for all accounts.") commit c1f7fa213b48fd71c2bf82af8dc71d5cd6eac20a Author: bob walker Date: Thu Aug 29 17:01:13 2024 +0100 only do live accounts diff --git a/aws-metrics.py b/aws-metrics.py index f7b0a30..431fb6f 100644 --- a/aws-metrics.py +++ b/aws-metrics.py @@ -12,7 +12,7 @@ def get_cloudfront_log_size(cloudfront_client, s3_client, distribution_id): return 0 s3_bucket_name = logging_config['Bucket'].split('.')[0] - s3_bucket_prefix = logging_config['Prefix'] + s3_bucket_prefix = logging_config['DistributionConfig'].get('Logging', {}).get('Prefix', '') # Set time range for the last month end_date = datetime.now(timezone.utc) # Current UTC time, timezone-aware @@ -47,21 +47,24 @@ def count_ecs_containers(ecs_client): # Initialize AWS Organizations client org_client = boto3.client('organizations') -# Get all active accounts, handling pagination +# List of specific Organizational Units (OUs) +ou_ids = ['ou-h0vo-0waugyoz', 'ou-h0vo-32wgvd6w'] active_accounts = [] -next_token = None -while True: - if next_token: - response = org_client.list_accounts(NextToken=next_token) - else: - response = org_client.list_accounts() - - active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) - - next_token = response.get('NextToken') - if not next_token: - break +# Get all active accounts for the specified OUs, handling pagination +for ou_id in ou_ids: + next_token = None + while True: + if next_token: + response = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=next_token) + else: + response = org_client.list_accounts_for_parent(ParentId=ou_id) + + active_accounts.extend([acc for acc in response['Accounts'] if acc['Status'] == 'ACTIVE']) + + next_token = response.get('NextToken') + if not next_token: + break # Prepare CSV file with open('aws_account_metrics.csv', mode='w', newline='') as file: @@ -96,9 +99,9 @@ with open('aws_account_metrics.csv', mode='w', newline='') as file: # Get CloudFront log size cloudfront_size = 0 distributions = cloudfront_client.list_distributions() -# for dist in distributions.get('DistributionList', {}).get('Items', []): -# cloudfront_size += get_cloudfront_log_size(cloudfront_client, s3_client, dist['Id']) -# + for dist in distributions.get('DistributionList', {}).get('Items', []): + cloudfront_size += get_cloudfront_log_size(cloudfront_client, s3_client, dist['Id']) + # Write to CSV writer.writerow([account_id, account_email, ec2_count, ecs_count, cloudfront_size]) Process Group PGID: 75770