├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── IPv4UsageMonitoringforAWS.py ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !IPv4UsageMonitoringforAWS.py 4 | !README.md 5 | !LICENSE 6 | !CODE_OF_CONDUCT.md 7 | !CONTRIBUTING.md 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /IPv4UsageMonitoringforAWS.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | # software and associated documentation files (the "Software"), to deal in the Software 7 | # without restriction, including without limitation the rights to use, copy, modify, 8 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | import getopt 19 | import ipaddress 20 | import json 21 | import os 22 | import sys 23 | import time 24 | from multiprocessing import Process 25 | 26 | import boto3 27 | 28 | 29 | def describe_eips(region: str, access_key_id: str, secret_key: str, session_token: str) -> dict: 30 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 31 | aws_session_token=session_token, region_name=region) 32 | 33 | ec2_client = session.client('ec2', region_name=region) 34 | unassociated_eips = list() 35 | associated_eips = list() 36 | addresses = ec2_client.describe_addresses() 37 | for address in addresses['Addresses']: 38 | if 'CustomerOwnedIpv4Pool' in address.keys(): 39 | # BYoIP address 40 | pass 41 | elif 'AssociationId' not in address.keys(): 42 | # Amazon Owned Public IP that isn't associated to anything 43 | unassociated_eips.append(address['PublicIp']) 44 | else: 45 | # Amazon Owned Public IP that is associated to something 46 | associated_eips.append(address['PublicIp']) 47 | return {'associated': associated_eips, 'unassociated': unassociated_eips} 48 | 49 | 50 | def parse_interface_type(interface_data: dict) -> str: 51 | if interface_data['InterfaceType'] == 'network_load_balancer': 52 | return 'Network Load Balancer' 53 | elif interface_data['InterfaceType'] == 'nat_gateway': 54 | return 'NAT Gateway' 55 | elif interface_data['InterfaceType'] == 'efa': 56 | return 'Elastic Fabric Adapter' 57 | elif interface_data['InterfaceType'] == 'trunk': 58 | return 'Trunk ENI' 59 | elif interface_data['InterfaceType'] == 'load_balancer': 60 | return 'Application Load Balancer' 61 | elif interface_data['InterfaceType'] == 'vpc_endpoint': 62 | return 'PrivateLink Endpoint' 63 | elif interface_data['InterfaceType'] == 'branch': 64 | return 'Branch ENI' 65 | elif interface_data['InterfaceType'] == 'transit_gateway': 66 | return 'Transit Gateway Attachment ENI' 67 | elif interface_data['InterfaceType'] == 'lambda': 68 | return 'Lambda Function Interface' 69 | elif interface_data['InterfaceType'] == 'quicksight': 70 | return 'Quicksight Interface' 71 | elif interface_data['InterfaceType'] == 'global_accelerator_managed': 72 | return 'Global Accelerator Private Access Interface' 73 | elif interface_data['InterfaceType'] == 'api_gateway_managed': 74 | return 'API Gateway' 75 | elif interface_data['InterfaceType'] == 'gateway_load_balancer': 76 | return 'Gateway Load Balancer' 77 | elif interface_data['InterfaceType'] == 'gateway_load_balancer_endpoint': 78 | return 'Gateway Load Balancer Endpoint' 79 | elif interface_data['InterfaceType'] == 'iot_rules_managed': 80 | return 'IoT' 81 | elif interface_data['InterfaceType'] == 'aws_codestar_connections_managed': 82 | return 'CodeStar' 83 | # The following is basically a catch all 84 | elif 'Attachment' in interface_data.keys(): 85 | if 'InstanceId' in interface_data['Attachment'].keys(): 86 | if len(interface_data['Attachment']['InstanceId']) > 8: 87 | return 'EC2 Instance' 88 | elif 'Association' in interface_data.keys(): 89 | if interface_data['Association']['IpOwnerId'] == 'amazon-elb': 90 | if 'ELB app/' in interface_data['Description']: 91 | return 'Application Load Balancer' 92 | else: 93 | return 'Classic Load Balancer' 94 | else: 95 | if 'Description' in interface_data.keys(): 96 | if str(interface_data['Description'])[:4] == 'arn:': 97 | assumed_type = ' '.join(interface_data['Description'].split(':')[2]) 98 | return assumed_type 99 | elif ' ' in interface_data['Description']: 100 | assumed_type = ' '.join(interface_data['Description'].split(' ')[:2]) 101 | return assumed_type 102 | else: 103 | return 'Unknown Resource Type' 104 | else: 105 | return 'Unknown Resource Type' 106 | elif interface_data['InterfaceType'] == 'interface': 107 | return 'Unattached ENI' 108 | else: 109 | return 'Unknown Resource Type' 110 | 111 | 112 | def parse_security_group_cidrs(security_group_ids: list, region: str, access_key_id: str, secret_key: str, 113 | session_token: str) -> dict: 114 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 115 | aws_session_token=session_token, region_name=region) 116 | 117 | ec2_client = session.client('ec2', region_name=region) 118 | egress_prefixes = list() 119 | ingress_prefixes = list() 120 | paginator = ec2_client.get_paginator('describe_security_group_rules') 121 | iterator = paginator.paginate(Filters=[{'Name': 'group-id', 'Values': security_group_ids}]) 122 | for page in iterator: 123 | for rule in page['SecurityGroupRules']: 124 | # Get direct IPv4 references in security group and look for SSH port 125 | if 'CidrIpv4' in rule.keys(): 126 | is_ssh = False 127 | if rule['IpProtocol'] == 'tcp' and rule['FromPort'] == 22 and rule['ToPort'] == 22: 128 | is_ssh = True 129 | if rule['IsEgress']: 130 | egress_prefixes.append({'cidr': rule['CidrIpv4'], 'is_ssh': is_ssh}) 131 | else: 132 | ingress_prefixes.append({'cidr': rule['CidrIpv4'], 'is_ssh': is_ssh}) 133 | # Get IPv4 references from a prefix list in security group and look for SSH port 134 | elif 'PrefixListId' in rule.keys(): 135 | is_ssh = False 136 | if rule['IpProtocol'] == 'tcp' and rule['FromPort'] == 22 and rule['ToPort'] == 22: 137 | is_ssh = True 138 | prefix_list_info = ec2_client.describe_prefix_lists(PrefixListIds=[rule['PrefixListId']]) 139 | for prefix_list in prefix_list_info['PrefixLists']: 140 | for cidr in prefix_list['Cidrs']: 141 | if ipaddress.ip_network(cidr).version == 4: 142 | if rule['IsEgress']: 143 | egress_prefixes.append({'cidr': cidr, 'is_ssh': is_ssh}) 144 | else: 145 | ingress_prefixes.append({'cidr': cidr, 'is_ssh': is_ssh}) 146 | return {'egress': egress_prefixes, 'ingress': ingress_prefixes} 147 | 148 | 149 | def parse_subnet_routes(vpc_id: str, subnet_id: str, region: str, access_key_id: str, secret_key: str, 150 | session_token: str) -> bool: 151 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 152 | aws_session_token=session_token, region_name=region) 153 | 154 | ec2_client = session.client('ec2', region_name=region) 155 | has_public_route = False 156 | subnet_route_table = None 157 | main_rt = None 158 | paginator = ec2_client.get_paginator('describe_route_tables') 159 | iterator = paginator.paginate(Filters=[{'Name': 'vpc-id', 'Values': [vpc_id]}]) 160 | # Determine the subnet route table by looking for route tables with direct assignment and if not found using 161 | # the default VPC route table 162 | for page in iterator: 163 | for route_table in page['RouteTables']: 164 | for association in route_table['Associations']: 165 | if association['Main']: 166 | main_rt = route_table 167 | elif association['SubnetId'] == subnet_id: 168 | subnet_route_table = route_table 169 | if not subnet_route_table: 170 | subnet_route_table = main_rt 171 | # Look for routes to the Internet Gateway in the associated subnet route table 172 | for route in subnet_route_table['Routes']: 173 | if 'DestinationCidrBlock' in route.keys(): 174 | if is_public_address(route['DestinationCidrBlock']) and 'GatewayId' in route.keys(): 175 | if 'igw-' in route['GatewayId']: 176 | has_public_route = True 177 | elif 'DestinationPrefixListId' in route.keys(): 178 | if 'GatewayId' in route.keys(): 179 | if 'igw-' in route['GatewayId']: 180 | prefix_list_info = ec2_client.describe_prefix_lists( 181 | PrefixListIds=[route['DestinationPrefixListId']]) 182 | for prefix_list in prefix_list_info['PrefixLists']: 183 | for cidr in prefix_list['Cidrs']: 184 | if ipaddress.ip_network(cidr).version == 4: 185 | if is_public_address(cidr): 186 | has_public_route = True 187 | break 188 | return has_public_route 189 | 190 | 191 | def is_public_address(cidr_input: str) -> bool: 192 | # Build a more custom way to look for Public IPv4 addresses by ruling out that an IP is not equal to or a subnet of 193 | # a known bogon network. This is an assumption that may be error-prone and mark something a public IP because it is 194 | # not a known bogon network but still being used inside the VPC space 195 | result = True 196 | non_public_networks = ["192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", 197 | "169.254.0.0/16", "169.254.0.0/16", "192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24", 198 | "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4"] 199 | for non_public_network in non_public_networks: 200 | if ipaddress.ip_network(cidr_input).subnet_of(ipaddress.ip_network(non_public_network)): 201 | result = False 202 | break 203 | return result 204 | 205 | 206 | def describe_public_ips(associated_eip_list: list, region: str, access_key_id: str, secret_key: str, 207 | session_token: str) -> list: 208 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 209 | aws_session_token=session_token, region_name=region) 210 | 211 | ec2_client = session.client('ec2', region_name=region) 212 | known_subnets = dict() 213 | public_ips = list() 214 | paginator = ec2_client.get_paginator('describe_network_interfaces') 215 | iterator = paginator.paginate() 216 | for page in iterator: 217 | for network_interface in page['NetworkInterfaces']: 218 | eni_pub_ips = list() 219 | primary_pub_ip = None 220 | 221 | # Get the primary IP and if associated Public/EIP 222 | if 'Association' in network_interface.keys(): 223 | if 'PublicIp' in network_interface['Association']: 224 | if network_interface['Association']['PublicIp'] in associated_eip_list: 225 | ip_type = 'Elastic IP' 226 | else: 227 | ip_type = 'Public IP' 228 | eni_pub_ips.append({'Address': network_interface['Association']['PublicIp'], 'IP_type': ip_type}) 229 | primary_pub_ip = network_interface['Association']['PublicIp'] 230 | 231 | # Get any secondary IPs and if associated Public/EIPs 232 | if 'PrivateIpAddresses' in network_interface.keys(): 233 | for private_ip in network_interface['PrivateIpAddresses']: 234 | if 'Association' in private_ip.keys(): 235 | if 'PublicIp' in private_ip['Association']: 236 | if network_interface['Association']['PublicIp'] != primary_pub_ip: 237 | if network_interface['Association']['PublicIp'] in associated_eip_list: 238 | ip_type = 'Elastic IP' 239 | else: 240 | ip_type = 'Public IP' 241 | eni_pub_ips.append( 242 | {'Address': network_interface['Association']['PublicIp'], 'IP_type': ip_type}) 243 | 244 | # Enrich the data only if there are public IPs on the interface 245 | if eni_pub_ips: 246 | 247 | # Set the initial suspicion level and reason in case of no findings 248 | suspicion = 'LOW' 249 | reason = str() 250 | 251 | # Determine what type of service the interface belongs to 252 | 253 | interface_type = parse_interface_type(network_interface) 254 | 255 | # Get some standard information about the interface 256 | vpc_id = network_interface['VpcId'] 257 | subnet_id = network_interface['SubnetId'] 258 | owner_account_id = network_interface['OwnerId'] 259 | interface_id = network_interface['NetworkInterfaceId'] 260 | 261 | # Parse the Security groups on the interface and look for reasons to suspect necessity or not 262 | security_groups = list() 263 | for security_group in network_interface['Groups']: 264 | security_groups.append(security_group['GroupId']) 265 | if security_groups: 266 | sg_cidrs = parse_security_group_cidrs(security_groups, region, access_key_id, secret_key, 267 | session_token) 268 | has_public_ingress_cidr = False 269 | for cidr in sg_cidrs['ingress']: 270 | if is_public_address(cidr['cidr']): 271 | has_public_ingress_cidr = True 272 | 273 | has_public_egress_cidr = False 274 | for cidr in sg_cidrs['egress']: 275 | if is_public_address(cidr['cidr']): 276 | has_public_egress_cidr = True 277 | 278 | if not has_public_egress_cidr and not has_public_ingress_cidr: 279 | suspicion = 'HIGH' 280 | reason += (' There are no public IPs allowed in either ingress or egress security group ' 281 | 'rules on the interface.') 282 | elif not has_public_ingress_cidr: 283 | suspicion = 'MODERATE' 284 | reason += (' There are no public IP addresses allowed for ingress in the security ' 285 | 'groups attached.') 286 | elif has_public_ingress_cidr: 287 | has_not_port_22 = False 288 | has_port_22 = False 289 | for cidr in sg_cidrs['ingress']: 290 | if is_public_address(cidr['cidr']): 291 | if cidr['is_ssh']: 292 | has_port_22 = True 293 | else: 294 | has_not_port_22 = True 295 | if has_port_22 and not has_not_port_22: 296 | suspicion = 'MODERATE' 297 | reason += 'Only port 22 is allowed inbound, consider EC2 Instance connect.' 298 | 299 | # Parse the subnets and look for reasons to suspect public IPs aren't needed 300 | if subnet_id in known_subnets.keys(): 301 | if known_subnets[subnet_id] == 'NOROUTE': 302 | suspicion = 'HIGH' 303 | reason += ' There is no route to an Internet Gateway for the subnet the ENI resides.' 304 | else: 305 | has_public_route = parse_subnet_routes(vpc_id, subnet_id, region, access_key_id, secret_key, 306 | session_token) 307 | if not has_public_route: 308 | known_subnets[subnet_id] = 'NOROUTE' 309 | suspicion = 'HIGH' 310 | reason += ' There is no route to an Internet Gateway for the subnet the ENI resides.' 311 | else: 312 | known_subnets[subnet_id] = 'PUBLICROUTE' 313 | for pub_ip in eni_pub_ips: 314 | pub_ip['VpcId'] = vpc_id 315 | pub_ip['SubnetId'] = subnet_id 316 | pub_ip['OwnerAccountId'] = owner_account_id 317 | pub_ip['InterfaceId'] = interface_id 318 | pub_ip['InterfaceType'] = interface_type 319 | pub_ip['Suspicion'] = suspicion 320 | pub_ip['Reason'] = reason 321 | public_ips.append(pub_ip) 322 | return public_ips 323 | 324 | 325 | def private_subnets_with_public_ips(region: str, access_key_id: str, secret_key: str, session_token: str) -> list: 326 | suspicious_subnets = list() 327 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 328 | aws_session_token=session_token, region_name=region) 329 | 330 | ec2_client = session.client('ec2', region_name=region) 331 | paginator = ec2_client.get_paginator('describe_subnets') 332 | iterator = paginator.paginate(Filters=[{'Name': 'map-public-ip-on-launch', 'Values': ['true']}]) 333 | for page in iterator: 334 | for subnet in page['Subnets']: 335 | subnet_id = subnet['SubnetId'] 336 | vpc_id = subnet['VpcId'] 337 | owner_id = subnet['OwnerId'] 338 | if not parse_subnet_routes(vpc_id, subnet_id, region, access_key_id, secret_key, session_token): 339 | suspicious_subnets.append({"SubnetId": subnet_id, "VpcId": vpc_id, "OwnerId": owner_id}) 340 | 341 | return suspicious_subnets 342 | 343 | 344 | def describe_vpn_connections(region: str, access_key_id: str, secret_key: str, session_token: str) -> list: 345 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 346 | aws_session_token=session_token, region_name=region) 347 | 348 | ec2_client = session.client('ec2', region_name=region) 349 | vpn_connections = list() 350 | vpns = ec2_client.describe_vpn_connections() 351 | for vpn_connection in vpns['VpnConnections']: 352 | outside_ip_1 = vpn_connection['Options']['TunnelOptions'][0]['OutsideIpAddress'] 353 | outside_ip_2 = vpn_connection['Options']['TunnelOptions'][1]['OutsideIpAddress'] 354 | if is_public_address(outside_ip_1) or is_public_address(outside_ip_2): 355 | vpn_connections.append({'ConnectionID': vpn_connection['VpnConnectionId'], 'OutsideIP1': outside_ip_1, 356 | 'OutsideIP2': outside_ip_2}) 357 | return vpn_connections 358 | 359 | 360 | def describe_global_accelerators(region: str, access_key_id: str, secret_key: str, session_token: str) -> list: 361 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 362 | aws_session_token=session_token, region_name='us-west-2') 363 | 364 | aga_client = session.client('globalaccelerator', region_name='us-west-2') 365 | global_accelerators = list() 366 | accelerators = aga_client.list_accelerators() 367 | for accelerator in accelerators['Accelerators']: 368 | aga_ip_list = list() 369 | for ip in accelerator['IpSets']: 370 | if ip['IpAddressFamily'] == 'IPv4': 371 | for addr in ip['IpAddresses']: 372 | aga_ip_list.append(addr) 373 | if len(aga_ip_list) == 2: 374 | account_id = accelerator['AcceleratorArn'].split(':')[4] 375 | global_accelerators.append( 376 | {'owner_id': account_id, 'arn': accelerator['AcceleratorArn'], 'addr1': aga_ip_list[0], 377 | 'addr2': aga_ip_list[1]}) 378 | return global_accelerators 379 | 380 | 381 | def thread_runner(region: str, access_key_id: str, secret_key: str, session_token: str, start_time: str, 382 | account_id: str = None, output_path: str = None): 383 | # Determine the base filename 384 | if account_id and output_path: 385 | file_name_base = output_path + '_' + start_time + '_' + account_id + '_' + region 386 | elif account_id: 387 | file_name_base = start_time + '_' + account_id + '_' + region 388 | elif output_path: 389 | file_name_base = output_path + '_' + start_time + '_' + region 390 | else: 391 | file_name_base = start_time + '_' + region 392 | 393 | # Get the EIP information 394 | eips = describe_eips(region, access_key_id, secret_key, session_token) 395 | 396 | unassociated_eip_output = str() 397 | for eip in eips['unassociated']: 398 | if account_id: 399 | unassociated_eip_output += account_id + ',' + region + ',' + eip + '\n' 400 | else: 401 | unassociated_eip_output += region + ',' + eip + '\n' 402 | with open(file_name_base + '_unassociated_eips.csv', 'w') as f: 403 | f.write(unassociated_eip_output) 404 | 405 | associated_eip_output = str() 406 | for eip in eips['associated']: 407 | if account_id: 408 | associated_eip_output += account_id + ',' + region + ',' + eip + '\n' 409 | else: 410 | associated_eip_output += region + ',' + eip + '\n' 411 | with open(file_name_base + '_associated_eips.csv', 'w') as f: 412 | f.write(associated_eip_output) 413 | 414 | # Get the public IP information 415 | eni_public_ips = describe_public_ips(eips['associated'], region, access_key_id, secret_key, session_token) 416 | eni_pub_ip_output = str() 417 | 418 | for eni_pub_ip in eni_public_ips: 419 | eni_pub_ip_output += ','.join( 420 | [eni_pub_ip['OwnerAccountId'], region, eni_pub_ip['InterfaceId'], eni_pub_ip['Address'], 421 | eni_pub_ip['VpcId'], eni_pub_ip['SubnetId'], eni_pub_ip['InterfaceType'], eni_pub_ip['IP_type'], 422 | eni_pub_ip['Suspicion'], eni_pub_ip['Reason']]) + '\n' 423 | 424 | with open(file_name_base + '_eni_public_ips.csv', 'w') as f: 425 | f.write(eni_pub_ip_output) 426 | 427 | # Get subnets with public IP enabled but no route to an IGW 428 | suspect_subnets = private_subnets_with_public_ips(region, access_key_id, secret_key, session_token) 429 | subnet_output = str() 430 | for subnet in suspect_subnets: 431 | subnet_output += ','.join([subnet['OwnerId'], region, subnet['VpcId'], subnet['SubnetId']]) + '\n' 432 | 433 | with open(file_name_base + '_private_subnets_with_auto_assign.csv', 'w') as f: 434 | f.write(subnet_output) 435 | 436 | # Get VPN connections with public IPs 437 | vpn_connections = describe_vpn_connections(region, access_key_id, secret_key, session_token) 438 | vpn_output = str() 439 | for vpn in vpn_connections: 440 | if account_id: 441 | vpn_output += account_id + ',' + region + ',' + vpn['ConnectionID'] + ',' + vpn['OutsideIP1'] + ',' + vpn[ 442 | 'OutsideIP2'] + '\n' 443 | else: 444 | vpn_output += region + ',' + vpn['ConnectionID'] + ',' + vpn['OutsideIP1'] + ',' + vpn['OutsideIP2'] + '\n' 445 | 446 | with open(file_name_base + '_vpn_connections.csv', 'w') as f: 447 | f.write(vpn_output) 448 | 449 | if region == 'us-west-2': 450 | # Get Global Accelerators with IPv4 451 | global_accelerators = describe_global_accelerators(region, access_key_id, secret_key, session_token) 452 | global_accelerator_output = str() 453 | for ga in global_accelerators: 454 | global_accelerator_output += ','.join([ga['owner_id'], ga['arn'], ga['addr1'], ga['addr2']]) + '\n' 455 | 456 | with open(file_name_base + '_global_accelerators.csv', 'w') as f: 457 | f.write(global_accelerator_output) 458 | 459 | 460 | def file_region_concatenator(regions: list, file_name: str, file_base: str, error_msg: str, file_header: str = None): 461 | with open(file_base + '_' + file_name, 'a') as f: 462 | if file_header: 463 | f.write(file_header + '\n') 464 | for region in regions: 465 | if file_name == 'global_accelerators.csv' and region != 'us-west-2': 466 | pass 467 | else: 468 | try: 469 | with open(file_base + '_' + region + '_' + file_name, 'r') as readfile: 470 | f.write(readfile.read()) 471 | if os.path.exists(file_base + '_' + region + '_' + file_name): 472 | os.remove(file_base + '_' + region + '_' + file_name) 473 | except: 474 | print('No file for ' + error_msg + ' in ' + region) 475 | pass 476 | 477 | 478 | def file_account_concatenator(accounts: list, file_name: str, file_base: str, error_msg: str, file_header: str): 479 | with open(file_base + '_' + file_name, 'a') as f: 480 | f.write(file_header + '\n') 481 | for account in accounts: 482 | try: 483 | with open(file_base + '_' + account + '_' + file_name, 'r') as readfile: 484 | f.write(readfile.read()) 485 | if os.path.exists(file_base + '_' + account + '_' + file_name): 486 | os.remove(file_base + '_' + account + '_' + file_name) 487 | except: 488 | print('No file for ' + error_msg + ' in ' + account) 489 | 490 | 491 | def single_account(args, side_access_key_id: str = None, side_secret_key: str = None, side_session_token: str = None, 492 | side_output_path: str = None, side_regions: str = None, side_account: str = None, 493 | start_time: str = None) -> None: 494 | if not start_time: 495 | start_time = str(int(time.time())) 496 | try: 497 | opts, args = getopt.getopt(args, '', ['profile=', 'regions=', 'access-key-id=', 'secret-key=', 'session-token=', 498 | 'output-path=']) 499 | except: 500 | print("Error") 501 | sys.exit(1) 502 | boto_profile = None 503 | regions_input = None 504 | access_key_id = None 505 | secret_key = None 506 | session_token = None 507 | output_path = None 508 | for opt, arg in opts: 509 | if opt in ['--profile']: 510 | boto_profile = arg 511 | elif opt in ['--regions']: 512 | regions_input = arg 513 | elif opt in ['--access-key-id']: 514 | access_key_id = arg 515 | elif opt in ['--secret-key']: 516 | secret_key = arg 517 | elif opt in ['--session-token']: 518 | session_token = arg 519 | elif opt in ['--output-path']: 520 | output_path = arg 521 | 522 | if side_secret_key: 523 | access_key_id = side_access_key_id 524 | if side_access_key_id: 525 | secret_key = side_secret_key 526 | if side_session_token: 527 | session_token = side_session_token 528 | if side_output_path: 529 | output_path = side_output_path 530 | if side_regions: 531 | regions_input = side_regions 532 | 533 | if boto_profile: 534 | if access_key_id and secret_key: 535 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 536 | aws_session_token=session_token, profile_name=boto_profile) 537 | else: 538 | session = boto3.session.Session(profile_name=boto_profile) 539 | elif access_key_id and secret_key: 540 | session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 541 | aws_session_token=session_token) 542 | else: 543 | session = boto3.session.Session() 544 | regions = list() 545 | ec2_regions_client = session.client('ec2', region_name='us-east-1') 546 | ec2_regions = ec2_regions_client.describe_regions()['Regions'] 547 | for region in ec2_regions: 548 | if region['OptInStatus'] != 'not-opted-in': 549 | regions.append(region['RegionName']) 550 | if regions_input: 551 | region_inputs_validated = list() 552 | region_inputs = regions_input.split(',') 553 | for region in region_inputs: 554 | if region in regions: 555 | region_inputs_validated.append(region) 556 | regions = region_inputs_validated 557 | if not access_key_id: 558 | access_key_id = session.get_credentials().access_key 559 | if not secret_key: 560 | secret_key = session.get_credentials().secret_key 561 | if not session_token: 562 | session_token = session.get_credentials().token 563 | procs = list() 564 | for region in regions: 565 | procs.append(Process(target=thread_runner, args=( 566 | region, access_key_id, secret_key, session_token, start_time, side_account, output_path))) 567 | 568 | for proc in procs: 569 | proc.start() 570 | for proc in procs: 571 | proc.join() 572 | 573 | # Determine the base filename 574 | if side_account and output_path: 575 | file_name_base = output_path + '_' + start_time + '_' + side_account 576 | elif side_account: 577 | file_name_base = start_time + '_' + side_account 578 | elif output_path: 579 | file_name_base = output_path + '_' + start_time 580 | else: 581 | file_name_base = start_time 582 | 583 | if side_account: 584 | file_names = ['unassociated_eips.csv', 'associated_eips.csv', 'eni_public_ips.csv', 585 | 'private_subnets_with_auto_assign.csv', 'vpn_connections.csv', 'global_accelerators.csv'] 586 | for fname in file_names: 587 | error_msg_text = fname.replace('_', ' ').replace('.csv', '') 588 | file_region_concatenator(regions, fname, file_name_base, error_msg_text) 589 | 590 | if not side_account: 591 | file_names = list() 592 | file_names.append({'file_name': 'unassociated_eips.csv', 'header': 'Region, EIP'}) 593 | file_names.append({'file_name': 'associated_eips.csv', 'header': 'Region, EIP'}) 594 | file_names.append({'file_name': 'eni_public_ips.csv', 595 | 'header': 'OwnerAccountId, Region, InterfaceId, Address, VpcId, SubnetId, InterfaceType, ' 596 | 'AddressType, SuspectUnnecessary, Reason'}) 597 | file_names.append( 598 | {'file_name': 'private_subnets_with_auto_assign.csv', 'header': 'OwnerAccountId, Region, VpcId, SubnetId'}) 599 | file_names.append({'file_name': 'vpn_connections.csv', 'header': 'Region, ConnectionId, Address1, Address2'}) 600 | file_names.append({'file_name': 'global_accelerators.csv', 'header': 'OwnerAccountId, Arn, Address1, Address2'}) 601 | for fname in file_names: 602 | file_name = fname['file_name'] 603 | file_header = fname['header'] 604 | error_msg_text = file_name.replace('_', ' ').replace('.csv', '') 605 | file_region_concatenator(regions, file_name, file_name_base, error_msg_text, file_header) 606 | 607 | 608 | def multi_account(args): 609 | start = str(int(time.time())) 610 | try: 611 | opts, args = getopt.getopt(args, '', ['profile=', 'regions=', 'accounts=', 'access-key-id=', 'secret-key=', 612 | 'session-token=', 'external-id=', 'role-name=', 'output-path=']) 613 | except: 614 | print("Error") 615 | sys.exit(1) 616 | boto_profile = None 617 | regions_input = None 618 | accounts = None 619 | access_key_id = None 620 | secret_key = None 621 | session_token = None 622 | output_path = None 623 | external_id = None 624 | role_name = None 625 | for opt, arg in opts: 626 | if opt in ['--profile']: 627 | boto_profile = arg 628 | elif opt in ['--regions']: 629 | regions_input = arg 630 | elif opt in ['--accounts']: 631 | accounts = arg 632 | elif opt in ['--access-key-id']: 633 | access_key_id = arg 634 | elif opt in ['--secret-key']: 635 | secret_key = arg 636 | elif opt in ['--session-token']: 637 | session_token = arg 638 | elif opt in ['--output-path']: 639 | output_path = arg 640 | elif opt in ['--external-id']: 641 | external_id = arg 642 | elif opt in ['--role-name']: 643 | role_name = arg 644 | 645 | if boto_profile: 646 | if access_key_id and secret_key: 647 | local_v1_session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 648 | aws_session_token=session_token, profile_name=boto_profile) 649 | else: 650 | local_v1_session = boto3.session.Session(profile_name=boto_profile) 651 | elif access_key_id and secret_key: 652 | local_v1_session = boto3.session.Session(aws_access_key_id=access_key_id, aws_secret_access_key=secret_key, 653 | aws_session_token=session_token) 654 | else: 655 | local_v1_session = boto3.session.Session() 656 | sts_client = local_v1_session.client('sts') 657 | local_session = local_v1_session.get_credentials() 658 | local_account = sts_client.get_caller_identity()['Account'] 659 | if not accounts: 660 | accounts = list() 661 | unchecked_accounts = list() 662 | orgs = local_v1_session.client('organizations', region_name='us-east-1') 663 | paginator = orgs.get_paginator('list_accounts') 664 | iterator = paginator.paginate() 665 | for page in iterator: 666 | for account in page['Accounts']: 667 | if account['Status'] == 'ACTIVE': 668 | accounts.append(account['Id']) 669 | else: 670 | unchecked_accounts.append(account['Id']) 671 | if output_path: 672 | file_name = output_path + 'unchecked_accounts.csv' 673 | else: 674 | file_name = 'unchecked_accounts.csv' 675 | with open(file_name, 'w') as f: 676 | f.write('\n'.join(unchecked_accounts)) 677 | else: 678 | if ',' in accounts: 679 | accounts = str(accounts).replace(' ', '').split(',') 680 | elif len(accounts) > 14: 681 | print('invalid account specification, please specify a list of accounts seperated by a comma.') 682 | exit(1) 683 | else: 684 | accounts = list(str(accounts).replace(' ', '')) 685 | 686 | paginated_account_list = [accounts[i:i + 10] for i in range(0, len(accounts), 10)] 687 | 688 | session_policy = {"Version": "2012-10-17", "Statement": [{"Sid": "RequiredDescribeCalls", "Effect": "Allow", 689 | "Action": ["ec2:DescribeAddresses", 690 | "ec2:DescribeSecurityGroupRules", 691 | "ec2:DescribeRouteTables", 692 | "ec2:DescribePrefixLists", 693 | "ec2:DescribeNetworkInterfaces", 694 | "ec2:DescribeSubnets", 695 | "ec2:DescribeVpnConnections", 696 | "ec2:DescribeRegions", 697 | "globalaccelerator:ListAccelerators", ], 698 | "Resource": "*"}]} 699 | for page_of_accounts in paginated_account_list: 700 | procs = list() 701 | for account in page_of_accounts: 702 | if account == local_account: 703 | 704 | access_key_id = local_session.access_key 705 | secret_key = local_session.secret_key 706 | try: 707 | session_token = local_session.token 708 | except: 709 | session_token = None 710 | procs.append(Process(target=single_account, args=( 711 | None, access_key_id, secret_key, session_token, output_path, regions_input, account, start))) 712 | else: 713 | if role_name: 714 | role = "arn:aws:iam::" + str(account) + ":role/" + role_name 715 | else: 716 | role = "arn:aws:iam::" + str(account) + ":role/OrganizationAccountAccessRole" 717 | sts = local_v1_session.client('sts', region_name='us-east-1', 718 | endpoint_url='https://sts-fips.us-east-1.amazonaws.com') 719 | try: 720 | if external_id: 721 | role_info = sts.assume_role(RoleArn=role, RoleSessionName='IPv4UsageMonitoringForAWS', 722 | Policy=json.dumps(session_policy), DurationSeconds=3600, 723 | ExternalId=external_id) 724 | else: 725 | role_info = sts.assume_role(RoleArn=role, RoleSessionName='IPv4UsageMonitoringForAWS', 726 | Policy=json.dumps(session_policy), DurationSeconds=3600) 727 | except: 728 | print("ERROR: Unable to assume role for account `" + str(account) + "`") 729 | continue 730 | if 'Credentials' in role_info.keys(): 731 | access_key_id = role_info['Credentials']['AccessKeyId'] 732 | secret_key = role_info['Credentials']['SecretAccessKey'] 733 | session_token = role_info['Credentials']['SessionToken'] 734 | 735 | procs.append(Process(target=single_account, args=( 736 | None, access_key_id, secret_key, session_token, output_path, regions_input, account, start))) 737 | else: 738 | print("ERROR: Unable to assume role for account `" + str(account) + "`") 739 | for proc in procs: 740 | proc.start() 741 | for proc in procs: 742 | proc.join() 743 | 744 | if output_path: 745 | file_name_base = output_path + '_' + start 746 | else: 747 | file_name_base = start 748 | file_names = list() 749 | file_names.append({'file_name': 'unassociated_eips.csv', 'header': 'AccountId, Region, EIP'}) 750 | file_names.append({'file_name': 'associated_eips.csv', 'header': 'AccountId, Region, EIP'}) 751 | file_names.append({'file_name': 'eni_public_ips.csv', 752 | 'header': 'OwnerAccountId, Region, InterfaceId, Address, VpcId, SubnetId, InterfaceType, ' 753 | 'AddressType, SuspectUnnecessary, Reason'}) 754 | file_names.append( 755 | {'file_name': 'private_subnets_with_auto_assign.csv', 'header': 'OwnerAccountId, Region, VpcId, SubnetId'}) 756 | file_names.append( 757 | {'file_name': 'vpn_connections.csv', 'header': 'AccountId, Region, ConnectionId, Address1, Address2'}) 758 | file_names.append({'file_name': 'global_accelerators.csv', 'header': 'OwnerAccountId, Arn, Address1, Address2'}) 759 | for fname in file_names: 760 | file_name = fname['file_name'] 761 | file_header = fname['header'] 762 | error_msg_text = file_name.replace('_', ' ').replace('.csv', '') 763 | file_account_concatenator(accounts, file_name, file_name_base, error_msg_text, file_header) 764 | 765 | 766 | def arg_parser_parent(actionobj, argsobj=None): 767 | if actionobj not in ['run-single-account', 'run-multi-account', 'exit']: 768 | print("ERROR: Unknown action `" + actionobj + "`.") 769 | sys.exit(1) 770 | if actionobj == 'run-single-account': 771 | single_account(argsobj) 772 | if actionobj == 'run-multi-account': 773 | multi_account(argsobj) 774 | 775 | 776 | if __name__ == '__main__': 777 | if sys.argv[1:]: 778 | argv = sys.argv[2:] 779 | action = str(sys.argv[1]) 780 | arg_parser_parent(action, argv) 781 | else: 782 | print('Missing action: ["run-multi-account", "run-single-account"') 783 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPv4 Usage Monitoring for AWS 2 | 3 | This script allows customers to iterate through all regions and all accounts in an organization to enumerate all public IPs and flag certain IPs that may be unnecessary for further investigation. 4 | 5 | ### Announcements 6 | - Amazon VPC IPAM now includes native functionality to report on IPs across an entire organization. The difference between IPAM and this script is this script will report on probability that an IP is no longer needed based on attributes listed below. If you are just looking to report on what IPs are allocated to your accounts, it is recommended to use VPC IPAM instead of this script. See the announcement for VPC IPAM [here](https://aws.amazon.com/about-aws/whats-new/2023/11/amazon-vpc-address-manager-free-features-tier/). 7 | 8 | 9 | ## Requirements 10 | 11 | - Linux or Mac 12 | - Python3 with Boto3 AWS SDK 13 | - IAM access to either the local account and/or to assume role into other accounts in the organization 14 | 15 | ## Considerations 16 | 17 | - This script is a **BEST EFFORT** to identify all resources with public IPs however it is dependent on a number of API calls that can fail for a variety of reasons (eg. insufficient permissions, throttling, etc.). This should only be used to inform users of a means to prioritize which resources to look at first for considerations of reduction or elimination of public IPv4 address usage. For billing estimations, please use Cost and Usage Reports to estimate data as discussed [here](https://aws.amazon.com/blogs/aws/new-aws-public-ipv4-address-charge-public-ip-insights/). 18 | - This assumes that any IP in a security group that is not a Bogon network is a public IP. Thus, if you are using non-bogon networks as VPC CIDRs and allowing those CIDRs in a security group, it may not flag an ENI with a public IP that is only open to "Internal" resources. 19 | 20 | ## Permissions required 21 | 22 | There are two role types, the role of the "local user" which is running the script, if you are running in an organization, use a role from the management account. The other role is an "assumed role" which the local user assumes in other accounts to run against them. The following are the permissions needed for each: 23 | 24 | #### Local User: 25 | 26 | - ec2:DescribeAddresses 27 | - ec2:DescribeSecurityGroupRules 28 | - ec2:DescribeRouteTables 29 | - ec2:DescribePrefixLists 30 | - ec2:DescribeNetworkInterfaces 31 | - ec2:DescribeSubnets 32 | - ec2:DescribeVpnConnections 33 | - ec2:DescribeRegions 34 | - globalaccelerator:ListAccelerators 35 | - organizations:ListAccounts (Only applicable if using multi-account) 36 | - sts:AssumeRole (Only applicable if using multi-account) 37 | 38 | #### Assumed Role 39 | 40 | - ec2:DescribeAddresses 41 | - ec2:DescribeSecurityGroupRules 42 | - ec2:DescribeRouteTables 43 | - ec2:DescribePrefixLists 44 | - ec2:DescribeNetworkInterfaces 45 | - ec2:DescribeSubnets 46 | - ec2:DescribeVpnConnections 47 | - ec2:DescribeRegions 48 | - globalaccelerator:ListAccelerators 49 | 50 | ## Usage 51 | 52 | ### Single Account (local) mode 53 | 54 | This runs against the local account, without any assumption of roles outside the provided boto3 profile/default profile. 55 | 56 | `python3 IPv4UsageMonitoringforAWS.py run-single-account ` 57 | 58 | #### Options 59 | 60 | - #### Profile 61 | 62 | [Optional] Specify the AWS CLI/Boto3 profile you wish to use. If not specified the default profile, or attached role will be used. 63 | 64 | `--profile ` 65 | 66 | - #### Regions 67 | 68 | [Optional] Specify the AWS regions you wish to iterate through in a comma delineated list without spaces. if not specified all regions that are opted-in (either manually or by default) will be iterated through. 69 | 70 | `--regions ` 71 | 72 | - #### AWS Access Key ID 73 | 74 | [Optional] Specify the AWS IAM access key you wish to use. If not specified the profile specified or default profile, or attached role will be used. 75 | 76 | `--access-key-id ` 77 | 78 | - #### AWS Secret Key 79 | 80 | [Optional] Specify the AWS IAM secret key you wish to use. If not specified the profile specified or default profile, or attached role will be used. 81 | 82 | `--secret-key ` 83 | 84 | - #### AWS STS Token 85 | 86 | [Optional] Specify the AWS STS session token you wish to use. If not specified the profile specified or default profile, or attached role will be used. 87 | 88 | `--session-token ` 89 | 90 | - #### File output path 91 | 92 | [Optional] Specify the output path you wish to write the result files to (eg. "example/" or "/home/ec2-user/"). If not specified the files will be output to the directory which the script context is run from. 93 | 94 | `--output-path ` 95 | 96 | 97 | ### Multi-Account (Organizations) mode 98 | 99 | This runs against the specified accounts or all accounts within an AWS organization, using STS to assume a role in each account. 100 | 101 | `python3 IPv4UsageMonitoringforAWS.py run-multi-account ` 102 | 103 | #### Options 104 | 105 | - #### STS External ID 106 | 107 | [Optional] Specify the External ID you wish to use for STS assume role. This must be the same for all accounts. If not specified no External ID is used. 108 | 109 | `--external-id ` 110 | 111 | - #### Role Name 112 | 113 | [Optional] Specify the IAM role name you wish to use for STS assume role. This must be the same for all accounts. If not specified the default `OrganizationAccountAccessRole` is used. 114 | 115 | `--role-name ` 116 | 117 | - #### Accounts 118 | 119 | [Optional] Specify the AWS accounts you wish to iterate through. If not specified this will iterate through all accounts in the AWS organization. This must be run from the AWS Organization Management account if accounts aren't specified. 120 | 121 | `--accounts` 122 | 123 | 124 | - #### Profile 125 | 126 | [Optional] Specify the AWS CLI/Boto3 profile you wish to use. If not specified the default profile, or attached role will be used. 127 | 128 | `--profile ` 129 | 130 | - #### Regions 131 | 132 | [Optional] Specify the AWS regions you wish to iterate through in a comma delineated list without spaces. if not specified all regions that are opted-in (either manually or by default) will be iterated through. 133 | 134 | `--regions ` 135 | 136 | - #### AWS Access Key ID 137 | 138 | [Optional] Specify the AWS IAM access key you wish to use. If not specified the profile specified or default profile, or attached role will be used. 139 | 140 | `--access-key-id ` 141 | 142 | - #### AWS Secret Key 143 | 144 | [Optional] Specify the AWS IAM secret key you wish to use. If not specified the profile specified or default profile, or attached role will be used. 145 | 146 | `--secret-key ` 147 | 148 | - #### AWS STS Token 149 | 150 | [Optional] Specify the AWS STS session token you wish to use. If not specified the profile specified or default profile, or attached role will be used. 151 | 152 | `--session-token ` 153 | 154 | - #### File output path 155 | 156 | [Optional] Specify the output path you wish to write the result files to (eg. "example/" or "/home/ec2-user/"). If not specified the files will be output to the directory which the script context is run from. 157 | 158 | `--output-path ` 159 | 160 | 161 | ## Output 162 | 163 | All file outputs are prepended with the Epoch timestamp when they were run followed by an underscore (eg. 1695071739_vpn_connections.csv). Note there may be duplicates in these files due to resources shared across accounts such as shared subnets. 164 | 165 | #### {Epoch}_associated_eips.csv 166 | 167 | This file enumerates all EIPs that are associated to a resource, what region they are in and if run against multi-account, what account they are in. 168 | 169 | #### {Epoch}_unassociated_eips.csv 170 | 171 | This file enumerates all EIPs that are NOT associated to a resource, what region they are in and if run against multi-account, what account they are in. 172 | 173 | #### {Epoch}_eni_public_ips.csv 174 | 175 | This file enumerates all public IPs and EIPs that are associated to ENIs. It will output the following: 176 | 177 | - ENI owner account 178 | - Region 179 | - ENI Id 180 | - Public IP address (including EIP) 181 | - VPC Id 182 | - Subnet Id 183 | - Interface type (eg. Network Load Balancer, NAT Gateway, EC2 Instance, Etc) 184 | - Type of IP address (Public IP or Elastic IP) 185 | - The level of suspicion that the IP is unnecessary (Please confirm yourself before deleting) 186 | - The reason for the suspicion 187 | 188 | #### {Epoch}_private_subnets_with_auto_assign.csv 189 | 190 | This file enumerates all subnets that have Public IP auto-assignment enabled but no route to an Internet Gateway. NOTE: It does not flag the subnet if auto-assignment is disabled but ENIs still have public IPs manually assigned, those ENIs are flagged as high suspicion in `{Epoch}_eni_public_ips.csv`. It will output the subnet owner account ID, region, VPC Id and Subnet Id. 191 | 192 | #### {Epoch}_global_accelerators.csv 193 | 194 | This file enumerates all global accelerators, the owner account and associated public IPs 195 | 196 | #### {Epoch}_vpn_connections.csv 197 | 198 | This file enumerates all public VPN connections, associated public IPs. The owner account is identified if run in multi-account mode. 199 | 200 | 201 | ## Security 202 | 203 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 204 | 205 | ## License 206 | 207 | This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. 208 | --------------------------------------------------------------------------------