Files

1766 lines
156 KiB
Plaintext

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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@dxw.com>
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 <bob@randomness.org.uk>
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 <bob@randomness.org.uk>
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 <bob@randomness.org.uk>
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 <bob@randomness.org.uk>
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